diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0215e77 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @alpkom diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..f0d9b49 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,60 @@ +name: setup +description: Checkout repo, set up Python and uv, and install dependencies. +inputs: + token: + description: 'GitHub token for checkout (defaults to GITHUB_TOKEN)' + default: ${{ github.token }} + persist-credentials: + description: 'Whether to persist the token in git config after checkout' + default: 'false' + ref: + description: 'Git ref to checkout' + default: '' + fetch-depth: + description: 'Number of commits to fetch (0 = full history)' + default: '1' + python-version: + description: 'Python version' + default: '3.13' + index-username: + description: 'Username for the PyPI index' + default: '' + index-token: + description: 'Auth token for the PyPI index' + default: '' + +runs: + using: composite + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ inputs.token }} + persist-credentials: ${{ inputs.persist-credentials }} + ref: ${{ inputs.ref }} + fetch-depth: ${{ inputs.fetch-depth }} + + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ inputs.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 #v8.2.0 + - name: Install dependencies + shell: bash + env: + INDEX_USERNAME: ${{ inputs.index-username }} + INDEX_TOKEN: ${{ inputs.index-token }} + IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }} + run: | + if [[ "$IS_FORK" == "true" ]]; then + echo "Fork detected — syncing from PyPI without lockfile" + UV_INDEX_URL="https://pypi.org/simple/" \ + uv sync --all-packages --all-extras + else + echo "Syncing from Artifactory with lockfile" + UV_INDEX_ARTIFACTORY_USERNAME="${INDEX_USERNAME}" \ + UV_INDEX_ARTIFACTORY_PASSWORD="${INDEX_TOKEN}" \ + uv sync --all-packages --all-extras --locked + fi diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03c1ffa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +version: 2 +registries: + sap-artifactory: + type: python-index + url: https://common.repositories.cloud.sap/artifactory/api/pypi/build.releases.pypi/ + username: ${{secrets.DBOT_ARTIFACTORY_USERNAME}} + password: ${{secrets.DBOT_ARTIFACTORY_TOKEN}} + replaces-base: true +updates: + - package-ecosystem: uv + target-branch: 'main' + directory: / + registries: + - sap-artifactory + schedule: + interval: daily + time: '22:00' + cooldown: + default-days: 5 + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + target-branch: 'main' + directory: / + schedule: + interval: daily + time: '22:00' + cooldown: + default-days: 5 diff --git a/.github/scripts/extract_changelog.py b/.github/scripts/extract_changelog.py new file mode 100644 index 0000000..31af104 --- /dev/null +++ b/.github/scripts/extract_changelog.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import sys + +package, version = sys.argv[1], sys.argv[2] +sections = open(f"packages/{package}/RELEASE_NOTES.md").read().split("## ") +match = next((s for s in sections if s.startswith(version)), None) + +if not match: + sys.exit(f"No changelog entry found for version {version}") +print(match[len(version):].strip()) diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py new file mode 100644 index 0000000..2727bae --- /dev/null +++ b/.github/scripts/update_changelog.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import sys + +package, version, body = sys.argv[1], sys.argv[2], sys.argv[3] +path = f"packages/{package}/RELEASE_NOTES.md" +content = open(path).read() +sections = content.split("## ") +match = next((s for s in sections if s.startswith(version)), None) + +if not match: + sys.exit(f"No changelog entry found for version {version}") + +open(path, "w").write(content.replace(match, f"{version}\n\n{body.strip()}\n\n")) diff --git a/.github/workflows/auto-doc.yml b/.github/workflows/auto-doc.yml new file mode 100644 index 0000000..a6b7601 --- /dev/null +++ b/.github/workflows/auto-doc.yml @@ -0,0 +1,64 @@ +name: auto-doc + +on: + pull_request: ~ + workflow_dispatch: ~ + +jobs: + doc: + runs-on: ubuntu-latest + permissions: + contents: read + if: github.event.pull_request.head.repo.fork == false + + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ secrets.SAP_AI_SDK_BOT_CLIENT_ID }} + private-key: ${{ secrets.SAP_AI_SDK_BOT_PRIVATE_KEY }} + permission-contents: write + + - name: Setup + uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + ref: ${{ github.head_ref }} + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Configure git + env: + BOT_EMAIL: ${{ vars.SAP_AI_SDK_BOT_EMAIL }} + BOT_NAME: ${{ vars.SAP_AI_SDK_BOT_NAME }} + run: | + git config --local user.email "$BOT_EMAIL" + git config --local user.name "$BOT_NAME" + + - name: Generate docs + run: | + generate_docs() { + ( + cd packages/$1/docs + rm -f *.html + uv run python -m pydoc -w ../ || true + ) + } + + generate_docs base + generate_docs core + generate_docs gen + + - name: Commit and push + env: + HEAD_REF: ${{ github.head_ref }} + run: | + git add packages/base/docs packages/core/docs packages/gen/docs + if git diff --cached --quiet; then + echo "No documentation changes to commit" + else + git commit -m "docs: update pydoc3 documentation [skip ci]" + git push origin HEAD:"$HEAD_REF" + fi diff --git a/.github/workflows/blackduck.yml b/.github/workflows/blackduck.yml new file mode 100644 index 0000000..0319333 --- /dev/null +++ b/.github/workflows/blackduck.yml @@ -0,0 +1,67 @@ +name: blackduck + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + inputs: + branch: + description: 'Branch to scan (defaults to default branch).' + required: false + type: string + +permissions: {} + +env: + BLACKDUCK_SKIP_PHONE_HOME: true + +jobs: + blackduck-scan: + name: blackduck scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + timeout-minutes: 15 + + steps: + - name: Determine project version + id: version + env: + INPUT_BRANCH: ${{ github.event.inputs.branch }} + run: | + if [ -n "$INPUT_BRANCH" ]; then + VERSION="$INPUT_BRANCH" + else + VERSION="main" + fi + echo "Black Duck project version: $VERSION" + echo "project_version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Checkout repository at project version + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.version.outputs.project_version }} + persist-credentials: false + + - name: Run Black Duck SCA scan + uses: blackduck-inc/black-duck-security-scan@3fe94e4b3c3947bd21ba46b7da255d6d3638b325 # v2.10.0 + with: + blackducksca_url: https://sap.blackducksoftware.com + blackducksca_token: ${{ secrets.BLACKDUCK_API_TOKEN }} + blackducksca_scan_full: true + blackducksca_scan_failure_severities: 'BLOCKER,CRITICAL' + blackducksca_reports_sarif_create: true + blackducksca_reports_sarif_severities: 'BLOCKER,CRITICAL' + blackducksca_reports_sarif_groupSCAIssues: true + blackducksca_upload_sarif_report: true + github_token: ${{ secrets.GITHUB_TOKEN }} + detect_args: >- + --detect.project.name="${{ vars.BLACKDUCK_PROJECT_NAME }}" + --detect.project.version.name="${{ steps.version.outputs.project_version }}" + --detect.project.user.groups="${{ vars.BLACKDUCK_PROJECT_GROUP }}" + --detect.code.location.name="${{ vars.BLACKDUCK_PROJECT_NAME }}/${{ steps.version.outputs.project_version }}" + --detect.blackduck.signature.scanner.memory=4096 + --detect.timeout=6000 + --detect.blackduck.signature.scanner.arguments="--min-scan-interval=0" + --detect.excluded.directories='**/__pycache__,**/.venv,**/venv,**/env,**/dist,**/build,**/*.egg-info,**/test,**/tests,**/coverage' diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml new file mode 100644 index 0000000..b77e3c8 --- /dev/null +++ b/.github/workflows/bump.yml @@ -0,0 +1,49 @@ +name: bump + +on: + workflow_dispatch: + inputs: + package: + description: 'Package to bump' + required: true + type: choice + options: + - base + - core + - gen + +jobs: + bump: + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ secrets.SAP_AI_SDK_BOT_CLIENT_ID }} + private-key: ${{ secrets.SAP_AI_SDK_BOT_PRIVATE_KEY }} + permission-contents: write + + - name: Setup + uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + fetch-depth: 0 + + - name: Configure git + run: | + git config --local user.email "${{ vars.SAP_AI_SDK_BOT_EMAIL }}" + git config --local user.name "${{ vars.SAP_AI_SDK_BOT_NAME }}" + + - name: Bump versions and generate changelogs + working-directory: packages/${{ inputs.package }} + run: uv run cz bump --changelog + + - name: Push + working-directory: packages/${{ inputs.package }} + run: | + VERSION=$(uv run cz version --project) + git push origin main + git push origin "${{ inputs.package }}-v${VERSION}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..86d2203 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,176 @@ +name: ci + +on: + push: + branches: + - 'main' + pull_request: ~ + workflow_dispatch: ~ + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false # reenable once teardown is implemented + +permissions: + contents: read + pull-requests: read + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Run pylint + run: | + uv run pylint ai_api_client_sdk ai_core_sdk gen_ai_hub \ + --errors-only \ + --output-format=json \ + + - name: Check dependency licenses + run: uv run pip-licenses + + unit-tests: + name: 'unit tests (${{ matrix.package.name }}, ${{ matrix.python-version }})' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.13', '3.14'] + package: + - name: base + path: packages/base/tests + source: packages/base/ai_api_client_sdk + - name: core + path: packages/core/tests + source: packages/core/ai_core_sdk + - name: gen + path: packages/gen/tests + source: packages/gen/gen_ai_hub + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + python-version: ${{ matrix.python-version }} + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Run unit tests with coverage + id: run-tests + run: | + uv run pytest ${{ matrix.package.path }} \ + --cov=${{ matrix.package.source }} \ + --verbosity=2 \ + -ra \ + --disable-warnings + + - name: Summarize coverage + if: matrix.python-version == '3.13' + run: uv run coverage report --format=markdown >> $GITHUB_STEP_SUMMARY + + integration-tests-base: + name: 'integration tests (base)' + runs-on: ubuntu-latest + if: | + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Run base integration tests + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + XSUAA_AUTH_URL: ${{ secrets.XSUAA_AUTH_URL }} + XSUAA_CLIENT_ID: ${{ secrets.XSUAA_CLIENT_ID }} + XSUAA_CLIENT_SECRET: ${{ secrets.XSUAA_CLIENT_SECRET }} + XSUAA_X509_CERT_URL: ${{ secrets.XSUAA_X509_CERT_URL }} + XSUAA_X509_CERT: ${{ secrets.XSUAA_X509_CERT }} + XSUAA_X509_KEY: ${{ secrets.XSUAA_X509_KEY }} + CLUSTER_BASE_URL: ${{ secrets.CLUSTER_BASE_URL }} + TEST_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + OSS_KEY: ${{ secrets.OSS_KEY }} + OSS_SECRET: ${{ secrets.OSS_SECRET }} + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} + OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} + OSS_REGION: ${{ secrets.OSS_REGION }} + run: | + uv run pytest packages/base/integration_tests \ + --verbosity=2 \ + -ra \ + --disable-warnings \ + -m "not bedrock" + + integration-tests-core: + name: 'integration tests (core)' + runs-on: ubuntu-latest + if: | + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Run core integration tests + env: + AICORE_RESOURCE_GROUP: ${{ secrets.AICORE_RESOURCE_GROUP }} + AICORE_BASE_URL: ${{ secrets.AICORE_BASE_URL }} + AICORE_AUTH_URL: ${{ secrets.AICORE_AUTH_URL }} + AICORE_CLIENT_ID: ${{ secrets.AICORE_CLIENT_ID }} + AICORE_CLIENT_SECRET: ${{ secrets.AICORE_CLIENT_SECRET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + XSUAA_AUTH_URL: ${{ secrets.XSUAA_AUTH_URL }} + XSUAA_CLIENT_ID: ${{ secrets.XSUAA_CLIENT_ID }} + XSUAA_CLIENT_SECRET: ${{ secrets.XSUAA_CLIENT_SECRET }} + XSUAA_X509_CERT_URL: ${{ secrets.XSUAA_X509_CERT_URL }} + XSUAA_X509_CERT: ${{ secrets.XSUAA_X509_CERT }} + XSUAA_X509_KEY: ${{ secrets.XSUAA_X509_KEY }} + CLUSTER_BASE_URL: ${{ secrets.CLUSTER_BASE_URL }} + TEST_TENANT_ID: ${{ secrets.TEST_TENANT_ID }} + OSS_KEY: ${{ secrets.OSS_KEY }} + OSS_SECRET: ${{ secrets.OSS_SECRET }} + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} + OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} + OSS_REGION: ${{ secrets.OSS_REGION }} + run: | + uv run pytest packages/core/integration_tests \ + --verbosity=2 \ + -ra \ + --disable-warnings \ + -m "not bedrock" + + integration-tests-gen: + name: 'integration tests (gen)' + runs-on: ubuntu-latest + if: | + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Run gen integration tests + env: + AICORE_RESOURCE_GROUP: ${{ secrets.AICORE_RESOURCE_GROUP }} + AICORE_BASE_URL: ${{ secrets.AICORE_BASE_URL }} + AICORE_AUTH_URL: ${{ secrets.AICORE_AUTH_URL }} + AICORE_CLIENT_ID: ${{ secrets.AICORE_CLIENT_ID }} + AICORE_CLIENT_SECRET: ${{ secrets.AICORE_CLIENT_SECRET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + uv run pytest packages/gen/integration_tests \ + --verbosity=2 \ + -ra \ + --disable-warnings \ + -m "not bedrock" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..04e86a0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +name: CodeQL + +on: + push: + branches: ['main', 'os-migration'] + pull_request: + branches: ['main', 'os-migration'] + schedule: + - cron: '20 6 * * 1' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..974fe9f --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,29 @@ +name: documentation + +on: + push: + tags: + - 'gen-v*' + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: self-hosted + permissions: + contents: read + + steps: + - name: Setup + uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + ref: ${{ github.ref_name }} + + - name: Make HTML + working-directory: packages/gen/docs + run: uv run make + + - name: Upload documentation artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: docs-${{ github.ref_name }} + path: packages/gen/docs/build/html diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml new file mode 100644 index 0000000..36cc4cd --- /dev/null +++ b/.github/workflows/draft-release.yml @@ -0,0 +1,45 @@ +name: draft-release + +on: + push: + tags: + - 'base-v*' + - 'core-v*' + - 'gen-v*' + +jobs: + draft-github-release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Get version from tag + id: get-version + run: | + TAG="${GITHUB_REF_NAME}" + PACKAGE="${TAG%%-v*}" + VERSION="${TAG#${PACKAGE}-v}" + echo "package=${PACKAGE}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Extract changelog + id: get-changelog + run: | + CHANGELOG=$(python3 .github/scripts/extract_changelog.py "${{ steps.get-version.outputs.package }}" "${{ steps.get-version.outputs.version }}") + echo "changelog<> "$GITHUB_OUTPUT" + echo "$CHANGELOG" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Create Draft Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGELOG: ${{ steps.get-changelog.outputs.changelog }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --verify-tag \ + --notes "$CHANGELOG" \ + --draft diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 0000000..3d589b1 --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,19 @@ +name: lint pr + +on: + pull_request: + types: + - opened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + lint-pr: + runs-on: ubuntu-latest + steps: + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9753ca1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,117 @@ +name: publish-testpypi + +on: + release: + types: [published] + +jobs: + sync-changelog: + name: Write release notes back to RELEASE_NOTES.md + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ secrets.SAP_AI_SDK_BOT_CLIENT_ID }} + private-key: ${{ secrets.SAP_AI_SDK_BOT_PRIVATE_KEY }} + permission-contents: write + + - name: Checkout main + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + token: ${{ steps.app-token.outputs.token }} + + - name: Get package and version from tag + id: meta + env: + TAG: ${{ github.event.release.tag_name }} + run: | + PACKAGE="${TAG%%-v*}" + VERSION="${TAG#${PACKAGE}-v}" + echo "package=${PACKAGE}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Update RELEASE_NOTES.md + env: + BODY: ${{ github.event.release.body }} + PACKAGE: ${{ steps.meta.outputs.package }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + python3 .github/scripts/update_changelog.py \ + "$PACKAGE" \ + "$VERSION" \ + "$BODY" + + - name: Configure git + env: + BOT_EMAIL: ${{ vars.SAP_AI_SDK_BOT_EMAIL }} + BOT_NAME: ${{ vars.SAP_AI_SDK_BOT_NAME }} + run: | + git config --local user.email "$BOT_EMAIL" + git config --local user.name "$BOT_NAME" + + - name: Commit and push + env: + TAG: ${{ github.event.release.tag_name }} + PACKAGE: ${{ steps.meta.outputs.package }} + run: | + git add "packages/${PACKAGE}/RELEASE_NOTES.md" + git diff --cached --quiet || git commit -m "docs: sync release notes for ${TAG} [skip ci]" + git push origin main + + build: + name: Build package + runs-on: ubuntu-latest + needs: sync-changelog + permissions: + contents: read + steps: + - name: Get package from tag + id: meta + env: + TAG: ${{ github.event.release.tag_name }} + run: | + PACKAGE="${TAG%%-v*}" + echo "package=${PACKAGE}" >> "$GITHUB_OUTPUT" + + - name: Setup + uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + ref: ${{ github.event.release.tag_name }} + + - name: Build package + env: + PACKAGE: ${{ steps.meta.outputs.package }} + run: | + uv build --package sap-ai-sdk-$PACKAGE --out-dir packages/$PACKAGE/dist + + - name: Upload distribution artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: dist + path: packages/${{ steps.meta.outputs.package }}/dist/ + + publish-pypi: + name: Publish to TestPyPI + runs-on: ubuntu-latest + needs: build + environment: + name: pypi + permissions: + id-token: write + steps: + - name: Download distribution artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: dist + path: dist/ + + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: false + packages-dir: dist/ diff --git a/.github/workflows/snapshot-release.yml b/.github/workflows/snapshot-release.yml new file mode 100644 index 0000000..5433760 --- /dev/null +++ b/.github/workflows/snapshot-release.yml @@ -0,0 +1,86 @@ +name: snapshot-release + +on: + # schedule: + # - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + snapshot-release: + runs-on: ubuntu-latest + concurrency: + group: snapshot-release-${{ github.ref }} + cancel-in-progress: true + permissions: + id-token: write + contents: read + env: + UV_INDEX_ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + UV_INDEX_ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_TOKEN }} + + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + fetch-depth: 0 + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Bump versions + run: | + now=$(date +%s) + + bump() ( + local pkg=$1 + + echo "Bumping $pkg..." + cd packages/$pkg + local old_version; old_version=$(uv run cz version --project) + uv run cz bump --version-files-only --prerelease alpha && code=0 || code=$? + + # exit code 3 = NO_COMMITS_FOUND; fall back to patch bump + if [ $code -eq 3 ]; then + echo "No commits found for $pkg, falling back to patch bump..." + uv run cz bump --version-files-only --prerelease alpha --increment PATCH + elif [ $code -ne 0 ]; then + echo "cz bump failed for $pkg with exit code $code" + exit $code + fi + + # Replace the a0 pre-release suffix with a timestamp to ensure uniqueness + sed -i "s/a0\"/a${now}\"/" pyproject.toml + + local version; version=$(uv run cz version --project) + echo "$pkg version: $old_version -> $version" + echo "- **sap-ai-sdk-${pkg}**: \`${old_version}\` → \`${version}\`" >> "$GITHUB_STEP_SUMMARY" + ) + + pin_dep() ( + local short=$1 target=$2 + local pkg="sap-ai-sdk-${short}" + local version; version=$(uv run --directory packages/$short cz version --project) + local project_toml="packages/${target}/pyproject.toml" + + # Replace any ~= or >= version constraint with an exact pin + sed -i "s/${pkg}[~>]=[^ ,\"]*/${pkg}==${version}/" "$project_toml" + echo "Pinned ${pkg}==${version} in ${target}" + ) + + bump base + pin_dep base core + + bump core + pin_dep core gen + + bump gen + + - name: Build packages + run: | + for pkg in base core gen; do + uv build --package sap-ai-sdk-$pkg --out-dir packages/$pkg/dist + done + + # - name: Release packages + # run: | + # for pkg in base core gen; do + # uv publish packages/$pkg/dist/ + # done diff --git a/.github/workflows/teardown-integration-env.yml b/.github/workflows/teardown-integration-env.yml new file mode 100644 index 0000000..1d87f0b --- /dev/null +++ b/.github/workflows/teardown-integration-env.yml @@ -0,0 +1,21 @@ +name: teardown-integration-env + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: ~ + +permissions: + contents: read + +jobs: + teardown: + runs-on: ubuntu-latest + steps: + - uses: SAP/ai-sdk-python/.github/actions/setup@os-migration + with: + index-username: ${{ secrets.ARTIFACTORY_USERNAME }} + index-token: ${{ secrets.ARTIFACTORY_TOKEN }} + + - name: Tear down integration test environment + run: bash scripts/teardown-integration-env.sh diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..3179df2 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,36 @@ +name: zizmor + +on: + pull_request: + paths: + - '.github/**' + push: + branches: + - 'main' + paths: + - '.github/**' + +permissions: + actions: read + contents: read + security-events: write + +jobs: + zizmor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install zizmor + run: pip install zizmor + + - name: Run zizmor (log) + run: zizmor --config zizmor.yml --min-severity high .github/ + + - name: Run zizmor (SARIF) + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + if: always() + with: + config: zizmor.yml + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12048ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +.env* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +**/share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +**/docs/_build/ +**/docs/source/_reference +**/docs/source/_api_doc + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +VENV +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# PyCharm +.idea/ + +# VS Code +.vscode/ + +# Certificates +*.pem diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 941c303..419cf09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,11 @@ Instances of abusive, harassing, or otherwise unacceptable behavior may be repor We use GitHub to manage reviews of pull requests. +There are different ways to contribute: + +* **Code**: add features, bug fixes, tests, or documentation changes. +* **Time**: support testing, triage, and collaboration with the maintainers. + * If you are a new contributor, see: [Steps to Contribute](#steps-to-contribute) * Before implementing your change, create an issue that describes the problem you would like to solve or the code that should be enhanced. Please note that you are willing to work on that issue. @@ -22,6 +27,17 @@ Should you wish to work on an issue, please claim it first by commenting on the If you have questions about one of the issues, please comment on them, and one of the maintainers will clarify. +Recommended flow for code contributions: + +1. Read the [Definition of Done](#definition-of-done) and project coding guidelines. +2. Align with maintainers on scope in the issue before starting larger changes. +3. Implement your contribution and add or update tests where relevant. +4. Open a pull request against `main` and explain: + - Reason for the change + - What was implemented and why + - Any migration or compatibility impact +5. Stay available to address review feedback and follow-up fixes. + ## Contributing Code or Documentation You are welcome to contribute code in order to fix a bug or to implement a new feature that is logged as an issue. @@ -32,6 +48,22 @@ The following rule governs code contributions: * Due to legal reasons, contributors will be asked to accept a Developer Certificate of Origin (DCO) when they create the first pull request to this project. This happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). * Contributions must follow our [guidelines on AI-generated code](https://github.com/SAP/.github/blob/main/CONTRIBUTING_USING_GENAI.md) in case you are using such tools. +## Definition of Done + +To keep quality and reliability high, contributions should meet these criteria: + +* Unit tests and integration tests pass for affected areas. +* CI checks are green for your pull request. +* Lint checks pass without new issues. + +## Documentation Expectations + +When adding features or changing behavior: + +* Update user-facing package documentation in [PYPIDESCRIPTION.md](PYPIDESCRIPTION.md) where applicable. +* Update or add inline code documentation where necessary. +* Update relevant documentation under [docs](docs) if the change affects usage. + ## Issues and Planning * We use GitHub issues to track bugs and enhancement requests. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a70f8c7 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +install: + uv sync --all-packages --all-extras + +lint: + uv run pylint ai_api_client_sdk ai_core_sdk gen_ai_hub --errors-only --output-format=colorized + +license-check: + uv run pip-licenses + +test: + uv run pytest packages/base/tests + uv run pytest packages/core/tests + uv run pytest packages/gen/tests + +test-integration: + uv run pytest packages/base/integration_tests + uv run pytest packages/core/integration_tests + uv run pytest packages/gen/integration_tests + +test-pkg: + uv run pytest packages/$(pkg)/tests + +test-pkg-integration: + uv run pytest packages/$(pkg)/integration_tests diff --git a/README.md b/README.md index 02ffad2..b42c7c1 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,76 @@ [![REUSE status](https://api.reuse.software/badge/github.com/SAP/ai-sdk-python)](https://api.reuse.software/info/github.com/SAP/ai-sdk-python) -# ai-sdk-python - -## About this project +# SAP Cloud SDK for AI (Python) SAP Cloud SDK for AI is the official Software Development Kit (SDK) for SAP AI Core, SAP Generative AI Hub, and Orchestration Service. -## Requirements and Setup +The SDK formerly known as generative AI Hub SDK was rebranded. + +## Installation + +Install the SDK with support for all model providers: + +```bash +pip install "sap-ai-sdk-gen[all]" +``` + +Install the default package (OpenAI support): + +```bash +pip install sap-ai-sdk-gen +``` + +Install selected extras: + +```bash +pip install "sap-ai-sdk-gen[google,amazon]" +``` + +For detailed configuration and usage examples, see [README_sphynx.md](README_sphynx.md). + +## Development + +Main SDK modules are under [gen_ai_hub](gen_ai_hub). -*Insert a short description what is required to get your project running...* +Integration tests are split into two groups: + +1. Standard integration tests. +2. Bedrock integration tests marked with the pytest marker bedrock. + +Run tests with: + +```bash +pytest tests -v +pytest integration_tests -m "not bedrock" -v +pytest integration_tests -m bedrock -v +``` + +You can also use the Makefile targets for acceptance test runs. + +## Documentation + +Documentation sources are in [docs](docs), and generated API docs are included there as well. + +For local documentation workflows and SDK configuration details, see [README_sphynx.md](README_sphynx.md). ## Support, Feedback, Contributing -This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/SAP/ai-sdk-python/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). +This project is open to feature requests, suggestions, and bug reports via [GitHub issues](https://github.com/SAP/ai-sdk-python/issues). + +Contribution and feedback are welcome. For contribution details, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Security / Disclosure -If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/SAP/ai-sdk-python/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems. + +If you find a potential security issue, please follow the process in [Security Policy](https://github.com/SAP/ai-sdk-python/security/policy). + +Please do not create public GitHub issues for security-related reports. ## Code of Conduct -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md) at all times. +By participating in this project, you agree to abide by the [Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md). ## Licensing -Copyright 2026 SAP SE or an SAP affiliate company and ai-sdk-python contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/SAP/ai-sdk-python). +Copyright 2026 SAP SE or an SAP affiliate company and ai-sdk-python contributors. + +See [LICENSE](LICENSE) for license information. Detailed third-party licensing information is available via the [REUSE tool](https://api.reuse.software/info/github.com/SAP/ai-sdk-python). diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e14bc0f --- /dev/null +++ b/TODO.md @@ -0,0 +1,25 @@ +- [x] remove all "requires more effort to fix" from zizmor +- [x] make repo compliant +- [ ] setup trusted publishing with PyPI +- [ ] replace Artifactory tokens with ones from the team +- [ ] ensure tear down always runs +- [ ] update status checks in branch protection rules +- [ ] add `__repr__` function after code is freshly moved + `_NamedPartial` + `NoDefault` + +```py +@dataclass +class CredentialsValue: + name: str + vcap_key: Optional[Tuple[str, ...]] = None + transform_fn: Optional[Callable] = None + + def __repr__(self): + fn = self.transform_fn.__name__ if self.transform_fn else None + return f"CredentialsValue(name={self.name!r}, vcap_key={self.vcap_key!r}, transform_fn={fn})" +``` + +after os-migration PR is merged: + +- [ ] create initial version tags (and delete old incorrect ones) +- [ ] use setup action from main not os-migration +- [ ] update gen version + release notes after internal version is published diff --git a/adr/0001-monorepo-tooling.md b/adr/0001-monorepo-tooling.md new file mode 100644 index 0000000..3354932 --- /dev/null +++ b/adr/0001-monorepo-tooling.md @@ -0,0 +1,52 @@ +# Monorepo Tooling + +## Status + +proposed + +## Context + +The SDK ships multiple related packages (`base`, `core`, `gen`) that share dev tooling, CI, and cross-package dependencies. We need a way to manage them in a single repository with consistent dependency resolution and a single lockfile. + +## Decision (preliminary) + +Use uv workspaces to manage the monorepo. Each package lives under `packages/` with its own `pyproject.toml`. The root `pyproject.toml` declares the workspace and shared index configuration. `uv sync --all-packages` installs all packages and their dependencies into a single shared `.venv`. + +## Consequences + +- Single lockfile (`uv.lock`) covers all packages — reproducible installs across the repo +- Cross-package dependencies resolve locally without publishing to a registry +- CI installs the entire workspace in one step +- uv is a required tool for contributors; there is no pip-based fallback + +# Appendix + +## Option A — uv workspaces (chosen) + +Root `pyproject.toml` declares `[tool.uv.workspace]` with `members = ["packages/*"]`. Each sub-package has its own `pyproject.toml` with its dependencies. `uv sync --all-packages --all-extras` installs everything. + +**Pros:** + +- Single lockfile, fast resolution +- Native cross-package local references via `[tool.uv.sources]` +- Only one tool needed (resolution, locking, venv, script running) + +**Cons:** + +- uv must be installed by all contributors and CI + +**Note:** `uv run` has no equivalent of `--all-packages` for running commands across all packages. Tools that accept multiple paths (e.g. pylint) can still be invoked once with all package paths; this is not uv-specific. + +## Option B — pip + virtual envs per package + +Each package manages its own venv. A top-level script or Makefile coordinates installs across packages. + +**Pros:** + +- Standard tooling, no uv dependency + +**Cons:** + +- No shared lockfile — dependency drift between packages +- Cross-package local installs require `pip install -e` per package in the right order (base → core → gen) +- CI must repeat the install step for each package diff --git a/adr/0002-interdependency-management.md b/adr/0002-interdependency-management.md new file mode 100644 index 0000000..6dd9097 --- /dev/null +++ b/adr/0002-interdependency-management.md @@ -0,0 +1,56 @@ +# Interdependency management + +## Status + +proposed + +## Context + +The monorepo contains three separately published packages: `sap-ai-sdk-base`, `sap-ai-sdk-core`, and `sap-ai-sdk-gen`. When the packages were merged from separate repositories into this monorepo, the question arose whether `sap-ai-sdk-gen` should continue to declare `sap-ai-sdk-core` as a runtime dependency or instead inline the core source code into the gen package at publish time. + +The packages were previously maintained as independent repositories and have distinct PyPI identities. Users interact with `sap-ai-sdk-core` directly — for example, by instantiating `AICoreV2Client` from `ai_core_sdk` — independently of whether they also use `sap-ai-sdk-gen`. + +## Decision + +`sap-ai-sdk-gen` declares `sap-ai-sdk-core` as a regular runtime dependency in its `pyproject.toml`. The core source code is not inlined or vendored into the gen package. During development, uv workspace sources resolve the dependency locally (`[tool.uv.sources] sap-ai-sdk-core = { workspace = true }`); at publish time, the dependency resolves from PyPI. + +## Consequences + +- Users who install `sap-ai-sdk-gen` automatically get a compatible version of `sap-ai-sdk-core` via pip's dependency resolution +- `sap-ai-sdk-core` can be installed and used independently without `sap-ai-sdk-gen` +- Types and classes from `ai_core_sdk` (e.g. `AICoreV2Client`) are the same objects regardless of whether the user reached them via `sap-ai-sdk-core` or `sap-ai-sdk-gen` — `isinstance` checks and type annotations work correctly across both packages +- Version constraints must be kept in sync when core introduces breaking changes +- Both packages must be released and published separately, in the correct order (core before gen) when there are cross-package changes + +# Appendix + +## Option A — Keep `sap-ai-sdk-core` as a declared dependency (chosen) + +`sap-ai-sdk-gen` lists `sap-ai-sdk-core>=x.y.z` in its `dependencies`. The monorepo uses `[tool.uv.sources]` to point at the local workspace copy during development. + +**Pros:** + +- Single source of truth for core code — no duplication, no drift +- Users installing only `sap-ai-sdk-core` get a standalone package with its own release cadence +- Object identity is preserved: a type imported from `ai_core_sdk` is the same type everywhere in a user's environment, so `isinstance` checks, type narrowing, and `typing.cast` all behave correctly +- Standard Python packaging convention; pip, uv, and other tools handle the transitive install automatically + +**Cons:** + +- Coordinated releases required: a breaking core change forces a gen release as well +- Version constraint management adds maintenance overhead over time + +## Option B — Inline (vendor) core source into gen at publish time + +Copy the `ai_core_sdk` source tree into the gen package wheel. Remove the `sap-ai-sdk-core` dependency from gen's metadata. + +**Pros:** + +- Gen ships as a single self-contained wheel with no same-org transitive dependency + +**Cons:** + +- Two copies of the same code on the user's system if they install both packages — leads to two separate `ai_core_sdk` namespaces and broken `isinstance` checks +- `sap-ai-sdk-core` is a public, user-facing package; inlining it into gen would create a hidden fork that silently diverges from the published version +- The vendoring step adds build complexity and is non-standard for first-party packages +- `sap-ai-sdk-core` itself depends on `sap-ai-sdk-base`, so the inline would need to be recursive or leave a dangling dependency diff --git a/adr/0003-versioning-and-release-strategy.md b/adr/0003-versioning-and-release-strategy.md new file mode 100644 index 0000000..15e7782 --- /dev/null +++ b/adr/0003-versioning-and-release-strategy.md @@ -0,0 +1,53 @@ +# Versioning and Release Strategy + +## Status + +proposed + +## Context + +The monorepo contains three separately published PyPI packages with a strict dependency chain: `sap-ai-sdk-base` ← `sap-ai-sdk-core` ← `sap-ai-sdk-gen`. Each package has an independent version today (`base` at 3.4.0, `core` at 3.3.0, `gen` at 7.0.0). + +Requirements: + +### Must haves + +- Publish packages in the correct topological order (base → core → gen) +- Update cross-package dependencies to latest (compatible) version + +### Nice to haves + +- Keep cross-package version constraints as wide as possible + +## Decision + +Pending. The options below are under evaluation. + +## Consequences + +To be filled in once a decision is made. + +# Appendix + +## Option A — Per-package independent releases with commitizen + orchestration scripts + +`commitizen` (`cz bump`) reads conventional commits since the last tag and derives a semver bump per package. The topological ordering and downstream constraint updates require bespoke orchestration scripts on top. + +**How it works in practice:** + +1. On release, a script determines which packages have unreleased commits. +2. Packages are processed in topological order (base → core → gen). For each changed package, `cz bump` writes the new version to `pyproject.toml` and generates a changelog entry. +3. If an upstream package bumped, the script updates the lower bound of the downstream constraint (e.g. `sap-ai-sdk-core>=3.3` → `>=3.4`) before bumping the downstream package, then publishes each in order. + +**Pros:** + +- Independent versions — only changed packages get a new version. +- Per-package changelogs and tags give a clear release history for each package independently. + +**Cons:** + +- Commitizen has no native monorepo mode — per-package scoping requires per-package tag prefixes and configuration. +- Downstream constraint updates and topological ordering must be scripted manually. +- A commit touching multiple packages is counted in each package's bump calculation independently, which can lead to over-bumping. + +--- diff --git a/adr/template.md b/adr/template.md new file mode 100644 index 0000000..05bbffb --- /dev/null +++ b/adr/template.md @@ -0,0 +1,34 @@ +# ADR template by Michael Nygard + +This is based on the template in [Documenting architecture decisions - Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +In each ADR file, write these sections: + +# Title + +## Status + +What is the status, such as active, outdated -> one sentence reason, superseded -> link to followup ADR. + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Consequences + +What becomes easier or more difficult to do because of this change? + +# Appendix [Optional] + +Details on the discussion leading to the decision. +Often a list of options with pros and cons including the selection implementation. + +## Option A + +## Option B + +... diff --git a/packages/base/PYPIDESCRIPTION.md b/packages/base/PYPIDESCRIPTION.md new file mode 100644 index 0000000..cb44f42 --- /dev/null +++ b/packages/base/PYPIDESCRIPTION.md @@ -0,0 +1,45 @@ + +# SAP Cloud SDK for AI (Python): Base Client for AI API +The SDK formerly known as *AI API Client SDK* was rebranded. + +The class names have not changed i.e., you can continue to use existing code. + +The Base Client for AI API is a Python-based SDK that enables you to access the AI API using Python methods and data +structures. + +The Base SDK can be used with any implementation of AI API. Because it is independent of the runtime implementation, it doesn't provide access to runtime-specific APIs. For example, maintaining object store secrets is specific to SAP AI Core and is therefore not included in the Base SDK. Check for SDK offerings for your runtime that let you access any runtime-specific APIs. For more information on the Core SDK see . + +For more information on the AI API specification, SAP AI Core and related topics, please refer to . + +## Example Usage + +```python +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client + +# Instantiate the client +client = AIAPIV2Client(base_url=AI_API_URL, + auth_url=AUTH_URL, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, resource_group=RESOURCE_GROUP) + +# Make some queries, e.g. +scenarios = client.scenario.query() + +for scenario in scenarios.resources: + print(scenario.id) + +# Find a deployable executables in scenario 1111 +executables = client.executable.query(scenario_id="1111") +for executable in executables.resources: + if executable.deployable == False: + break +print(executable.id) + +# Inspect the required parameters for the executable +for parameter in executable.parameters: + print("Parameter:{}, Type: {}".format(parameter.name, parameter.type)) + +# Create a configuration +parameter_epochs = ParameterBinding(key="training-epochs", value="1") +myConfiguration = client.configuration.create(name="test", scenario_id="1111" executable_id="argo-mnist-0.2.1", parameter_bindings=[parameter_epochs]) +``` diff --git a/packages/base/README.md b/packages/base/README.md new file mode 100644 index 0000000..a9ad652 --- /dev/null +++ b/packages/base/README.md @@ -0,0 +1,80 @@ +# SAP Cloud SDK for AI (Python): Base Client for AI API + +The SDK formerly known as *AI API Client SDK* was rebranded. + +Use the new package name to install the SDK: +``` +pip install sap-ai-sdk-base +``` +The class names have not changed i.e., you can continue to use existing code. + +Everything in ai_api_client_sdk folder will be packaged in the library. + +The main client class is the [AIAPIV2Client](ai_api_client_sdk/ai_api_v2_client.py). Each instance of AIAPIV2Client +has resource clients as properties. The resource client implementations can be found in folder [resource_clients](ai_api_client_sdk/resource_clients). +The resources, response types etc. are represented by model classes. These can be found in folder [models](ai_api_client_sdk/models). + +Renovate is set up for this repository. For further information, take a look at the [documentation in ml-api-facade](https://github.wdf.sap.corp/AI/ml-api-facade/blob/master/docs/renovate.md). + +## Usage + +The user can use the library by creating an instance of AIAPIV2Client class. There are some required and optional +parameters for the constructor of AIAPIV2Client class: + +- `base_url` (string) (required): The base URL of AI API. (i.e. https://api.ai.nonexistingcluster.com/v2/lm) +- `token_creator` (optional) (Callable): This should be a function which returns a token for authorization. Either this + function or auth_url, client_id and client_secret should be provided. +- `auth_url`: URL for creating the authorization token (i.e. https://blabla.authentication.sap.hana.ondemand.com/oauth/token) +- `client_id` (optional): clientid for xsuaa authentication +- `client_secret`(optional): clientsecret for xsuaa authentication +- `cert_str`(optional): certificate file content, needs to be provided alongside the key_str parameter +- `key_str` (optional): key file content, needs to be provided alongside the cert_str parameter +- `cert_file_path` (optional): path to the certificate file, needs to be provided alongside the key_file_path parameter +- `key_file_path` (optional): path to the key file, needs to be provided alongside the cert_file_path parameter +- `resource_group` (string) (optional): if provided, this will be used as default resource group id for requests to the AI API. + The user can still provide resource_group with every request to the AI API, + and that will override this one. + +The AIAPIV2Client will have a property per resource (each one is an instance of a resource_client): + +- `artifact` (an instance of [ArtifactClient](ai_api_client_sdk/resource_clients/artifact_client.py)) +- `configuration` (an instance of [ConfigurationClient](ai_api_client_sdk/resource_clients/configuration_client.py)) +- `deployment` (an instance of [DeploymentClient](ai_api_client_sdk/resource_clients/deployment_client.py)) +- `executable` (an instance of [ExecutableClient](ai_api_client_sdk/resource_clients/executable_client.py)) +- `execution` (an instance of [ExecutionClient](ai_api_client_sdk/resource_clients/execution_client.py)) +- `healthz` (an instance of [HealthzClient](ai_api_client_sdk/resource_clients/healthz_client.py)) +- `metrics` (an instance of [MetricsClient](ai_api_client_sdk/resource_clients/metrics_client.py)) +- `scenario` (an instance of [ScenarioClient](ai_api_client_sdk/resource_clients/scenario_client.py)) +- `resource_groups` (an instance of [ResourceGroupsClient](ai_api_client_sdk/resource_clients/resource_groups_client.py)) + +Each resource client has these functions (if supported for that resource) to send requests to the AI API: + +- create(*args, **kwargs): creates a resource +- delete(*args, **kwargs): deletes a resource +- get(*args, **kwargs): gets a single resource +- modify(*args, **kwargs): patches a resource +- query(*args, **kwargs): queries multiple resources + +Example: + +```python + +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client + +ai_api_v2_client = AIAPIV2Client( + base_url="", + auth_url="", + client_id="", + client_secret="", + resource_group="" +) + +scenario = ai_api_v2_client.scenario.get(scenario_id="") +``` + +## Tests + +The [unit tests](tests) are simply python unit tests. They can be run via pytest or directly from IDE. + +The [integration_tests](integration_tests) are also python tests. They run against intwdf cluster. + \ No newline at end of file diff --git a/packages/base/ai_api_client_sdk/__init__.py b/packages/base/ai_api_client_sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/ai_api_client_sdk/ai_api_v2_client.py b/packages/base/ai_api_client_sdk/ai_api_v2_client.py new file mode 100644 index 0000000..9de3eeb --- /dev/null +++ b/packages/base/ai_api_client_sdk/ai_api_v2_client.py @@ -0,0 +1,124 @@ +import os +from typing import Callable + +from ai_api_client_sdk.exception import AIAPIAuthenticatorException +from ai_api_client_sdk.helpers import _is_all_none +from ai_api_client_sdk.helpers.authenticator import Authenticator +from ai_api_client_sdk.helpers.constants import SKIP_AUTH_ENV_VAR, Timeouts +from ai_api_client_sdk.helpers.rest_client import RestClient +from ai_api_client_sdk.resource_clients import ( + ArtifactClient, + ConfigurationClient, + DeploymentClient, + ExecutableClient, + ExecutionClient, + ExecutionScheduleClient, + HealthzClient, + MetaClient, + MetricsClient, + ModelClient, + ResourceGroupsClient, + ScenarioClient, +) + +AUTH_PARAM_ERROR_MESSAGE = """ +For authorization please provide either one of the following options: +1. token_creator +2. auth_url, client_id and one of the following options: + a. client_secret + b. cert_str & key_str + c. cert_file_path & key_file_path +""" + + +class AIAPIV2Client: + """The AIAPIV2Client is the class implemented to interact with the AI API server. The user can use its attributes + corresponding to the resources, for interacting with endpoints related to that resource. (i.e., + aiapiv2client.scenario) + + :param base_url: Base URL of the AI API server. Should include the base path as well. (i.e., "/scenarios" + should work) + :type base_url: str + :param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to + None + :type auth_url: str, optional + :param client_id: client id to be used for authorization, defaults to None + :type client_id: str, optional + :param client_secret: client secret to be used for authorization, defaults to None + :type client_secret: str, optional + :param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None + :type cert_str: str, optional + :param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None + :type key_str: str, optional + :param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter, + defaults to None + :type cert_file_path: str, optional + :param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter, + defaults to None + :type key_file_path: str, optional + :param token_creator: the function which returns the Bearer token, when called. Either this, or + auth_url & client_id & client_secret should be specified, defaults to None + :type token_creator: Callable[[], str], optional + :param resource_group: The default resource group which will be used while sending the requests to the server. If + not set, the resource_group should be specified with every request to the server, defaults to None + :type resource_group: str, optional + :param read_timeout: Read timeout for requests in seconds, defaults to 60s + :type read_timeout: int + :param connect_timeout: Connect timeout for requests in seconds, defaults to 60s + :type connect_timeout: int + :param num_request_retries: Number of retries for failing requests with http status code 429, 500, 502, 503 or 504, + defaults to 60s + :type num_request_retries: int + :param client_type: Client type header to be sent in the request, defaults to 'AI API Python SDK' + :type client_type: str + """ + + @staticmethod + def _create_token_creator_if_does_not_exist(token_creator, auth_url, client_id, client_secret, cert_str, key_str, + cert_file_path, key_file_path): + if token_creator: + if not _is_all_none(auth_url, client_id, client_secret, cert_str, key_str, cert_file_path, key_file_path): + raise AIAPIAuthenticatorException(error_message=AUTH_PARAM_ERROR_MESSAGE) + return token_creator + try: + return Authenticator(auth_url=auth_url, client_id=client_id, client_secret=client_secret, cert_str=cert_str, + key_str=key_str, cert_file_path=cert_file_path, key_file_path=key_file_path).get_token + except TypeError as te: + if any(x in te.__str__() for x in ['auth_url', 'client_id']): + raise AIAPIAuthenticatorException(error_message=AUTH_PARAM_ERROR_MESSAGE) + raise te + except AIAPIAuthenticatorException: + raise AIAPIAuthenticatorException(error_message=AUTH_PARAM_ERROR_MESSAGE) + + def __init__(self, base_url: str, auth_url: str = None, client_id: str = None, client_secret: str = None, + cert_str: str = None, key_str: str = None, cert_file_path: str = None, key_file_path: str = None, + token_creator: Callable[[], str] = None, resource_group: str = None, + connect_timeout=Timeouts.CONNECT_TIMEOUT.value, + num_request_retries=Timeouts.NUM_REQUEST_RETRIES.value, + **kwargs): + read_timeout = kwargs.get('read_timeout', Timeouts.READ_TIMEOUT.value) + client_type = kwargs.get('client_type', 'AI API Python SDK') + + self.base_url: str = base_url + if os.environ.get(SKIP_AUTH_ENV_VAR, '').lower() != 'true': + token_creator = self._create_token_creator_if_does_not_exist( + token_creator=token_creator, auth_url=auth_url, client_id=client_id, client_secret=client_secret, + cert_str=cert_str, key_str=key_str, cert_file_path=cert_file_path, key_file_path=key_file_path) + self.rest_client: RestClient = RestClient(base_url=base_url, get_token=token_creator, + resource_group=resource_group, read_timeout=read_timeout, + connect_timeout=connect_timeout, + num_request_retries=num_request_retries, + client_type=client_type) + self.artifact: ArtifactClient = ArtifactClient(rest_client=self.rest_client) + self.configuration: ConfigurationClient = ConfigurationClient(rest_client=self.rest_client) + self.deployment: DeploymentClient = DeploymentClient(rest_client=self.rest_client) + self.executable: ExecutableClient = ExecutableClient(rest_client=self.rest_client) + self.execution: ExecutionClient = ExecutionClient(rest_client=self.rest_client) + self.execution_schedule: ExecutionScheduleClient = ExecutionScheduleClient(rest_client=self.rest_client) + self.healthz: HealthzClient = HealthzClient(rest_client=self.rest_client) + self.metrics: MetricsClient = MetricsClient(rest_client=self.rest_client) + self.model: ModelClient = ModelClient(rest_client=self.rest_client) + self.scenario: ScenarioClient = ScenarioClient(rest_client=self.rest_client) + self.meta: MetaClient = MetaClient(rest_client=self.rest_client) + admin_rest_client = RestClient(base_url=base_url[:-3], get_token=token_creator, resource_group=resource_group) + self.resource_groups: ResourceGroupsClient = ResourceGroupsClient(rest_client=admin_rest_client) diff --git a/packages/base/ai_api_client_sdk/exception.py b/packages/base/ai_api_client_sdk/exception.py new file mode 100644 index 0000000..5aead63 --- /dev/null +++ b/packages/base/ai_api_client_sdk/exception.py @@ -0,0 +1,150 @@ +class AIAPIClientSDKException(Exception): + """Base Exception class for AI API Client SDK exceptions""" + + def __init__(self, description: str, status_code: int = None, error_message: str = None): + msg = f'{description}: {error_message}' if error_message else description + super().__init__(msg) + self.description = description + self.status_code = status_code + self.error_message = error_message + + +class AIAPIInvalidInputException(AIAPIClientSDKException): + """Exception type raised, when the provided input is invalid""" + def __init__(self, description: str): + super().__init__(description=description) + + +class AIAPIAuthenticatorException(AIAPIClientSDKException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator`""" + + def __init__(self, status_code: int = None, error_message: str = None): + super().__init__(description='Could not retrieve Authorization token', status_code=status_code, + error_message=error_message) + + +class AIAPIAuthenticatorInvalidRequestException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with bad request when trying to retrieve a token""" + + def __init__(self, error_message: str = None): + super().__init__(status_code=400, error_message=error_message) + + +class AIAPIAuthenticatorAuthorizationException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with unauthorized when trying to retrieve a token""" + + def __init__(self, error_message: str = None): + super().__init__(status_code=401, error_message=error_message) + + +class AIAPIAuthenticatorForbiddenException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with forbidden when trying to retrieve a token""" + + def __init__(self, error_message: str = None): + super().__init__(status_code=403, error_message=error_message) + + +class AIAPIAuthenticatorMethodNotAllowedException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with method not allowed when trying to retrieve a token""" + + def __init__(self, error_message: str = None): + super().__init__(status_code=405, error_message=error_message) + + +class AIAPIAuthenticatorTimeoutException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with request timeout when trying to retrieve a token""" + + def __init__(self, error_message: str = None): + super().__init__(status_code=408, error_message=error_message) + + +class AIAPIAuthenticatorServerException(AIAPIAuthenticatorException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if + the XSUAA server responded with server error when trying to retrieve a token""" + + def __init__(self, status_code: int = None, error_message: str = None): + super().__init__(status_code=status_code, error_message=error_message) + + +class AIAPIServerException(Exception): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient`, if a non-2XX + response is received from the server. + + :param description: description of the exception + :type description: str + :param status_code: Status code of the response from the server + :type status_code: int + :param error_message: Error message received from the server + :type error_message: str + :param error_code: Error code received from the server, defaults to None + :type error_code: str, optional + :param request_id: ID of the request, the response belongs to, defaults to None + :type request_id: str, optional + :param details: Error details received from the server, defaults to None + :type details: dict, optional + """ + + def __init__(self, description: str, status_code: int, error_message: str, error_code: str = None, + request_id: str = None, details: dict = None): + super().__init__() + self.description = description + self.status_code = status_code + self.error_code = error_code + self.error_message = error_message + self.request_id = request_id + self.details = details + + def __str__(self): + debug_msg = f'{self.description}: {self.error_message} \n ' \ + f'Status Code: {self.status_code}, Request ID:{self.request_id}' + return debug_msg + + +class AIAPIAuthorizationException(AIAPIServerException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 401 response + is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there + for object definition + """ + + def __init__(self, description: str, error_message: str, error_code: str = None, request_id: str = None, + details: dict = None): + super().__init__(description=description, status_code=401, error_code=error_code, error_message=error_message, + request_id=request_id, details=details) + + +class AIAPIInvalidRequestException(AIAPIServerException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 400 response + is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there + for object definition + """ + + def __init__(self, description: str, error_code: str, error_message: str, request_id: str, details: dict = None): + super().__init__(description=description, status_code=400, error_code=error_code, error_message=error_message, + request_id=request_id, details=details) + + +class AIAPINotFoundException(AIAPIServerException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 404 response + is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there + for object definition + """ + + def __init__(self, description: str, error_code: str, error_message: str, request_id: str, details: dict = None): + super().__init__(description=description, status_code=404, error_code=error_code, error_message=error_message, + request_id=request_id, details=details) + + +class AIAPIPreconditionFailedException(AIAPIServerException): + """Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 412 response + is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there + for object definition + """ + + def __init__(self, description: str, error_code: str, error_message: str, request_id: str, details: dict = None): + super().__init__(description=description, status_code=412, error_code=error_code, error_message=error_message, + request_id=request_id, details=details) diff --git a/packages/base/ai_api_client_sdk/helpers/__init__.py b/packages/base/ai_api_client_sdk/helpers/__init__.py new file mode 100644 index 0000000..610f8d0 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/__init__.py @@ -0,0 +1,2 @@ +def _is_all_none(*args): + return all(x is None for x in args) diff --git a/packages/base/ai_api_client_sdk/helpers/authenticator.py b/packages/base/ai_api_client_sdk/helpers/authenticator.py new file mode 100644 index 0000000..5368089 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/authenticator.py @@ -0,0 +1,252 @@ +import os +import tempfile +from datetime import timedelta, datetime, timezone +from threading import Lock +from typing import Optional + +import requests +import time +from requests.exceptions import ConnectionError + +from ai_api_client_sdk.exception import AIAPIAuthenticatorException, AIAPIAuthenticatorInvalidRequestException, \ + AIAPIAuthenticatorAuthorizationException, AIAPIAuthenticatorServerException, \ + AIAPIAuthenticatorForbiddenException, AIAPIAuthenticatorMethodNotAllowedException, \ + AIAPIAuthenticatorTimeoutException +from ai_api_client_sdk.helpers import _is_all_none + +PARAM_ERROR_MESSAGE = ('Either client_secret, or (cert_file_path, key_file_path) pair, ' + 'or (cert_str, key_str) pair need to be provided') +MAX_RETRY_ATTEMPTS_FOR_TOKEN = 3 +BASE_DELAY_FOR_TOKEN_RETRY = 0.3 + + +class Authenticator: + """Authenticator class is implemented to retrieve and cache the authorization token from the xsuaa server + + :param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token) + :type auth_url: str + :param client_id: client id to be used for authorization + :type client_id: str + :param client_secret: client secret to be used for authorization, either client_secret or + (cert_file_path and key_file_path) need to be provided, defaults to None + :type client_secret: str, optional + :param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None + :type cert_str: str, optional + :param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None + :type key_str: str, optional + :param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter, + defaults to None + :type cert_file_path: str, optional + :param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter, + defaults to None + :type key_file_path: str, optional + """ + + def __init__(self, auth_url: str, client_id: str, client_secret: str = None, cert_str: str = None, + key_str: str = None, cert_file_path: str = None, key_file_path: str = None): + self.url: str = auth_url + self.client_id: str = client_id + self.client_secret: str = client_secret + self.cert_str: str = cert_str.replace('\\n', '\n') if cert_str else None + self.key_str: str = key_str.replace('\\n', '\n') if key_str else None + self.cert_file_path: str = cert_file_path + self.key_file_path: str = key_file_path + if not ((client_secret is not None and _is_all_none(cert_str, key_str, cert_file_path, key_file_path)) + or (cert_file_path is not None and key_file_path is not None and _is_all_none(client_secret, + cert_str, key_str)) + or (cert_str is not None and key_str is not None and _is_all_none(client_secret, cert_file_path, + key_file_path))): + raise AIAPIAuthenticatorException(error_message=PARAM_ERROR_MESSAGE) + self.token = None # Token Caching + self.token_expiry_date = None + self.lock = Lock() # Thread-Safe Lock + + def _request_token_with_cert_key_str(self, data: dict): + with tempfile.TemporaryDirectory() as temp_dir: + cert_file_path = os.path.join(temp_dir, f'cert.pem') + key_file_path = os.path.join(temp_dir, f'key.pem') + with open(cert_file_path, 'w') as f: + f.write(self.cert_str) + with open(key_file_path, 'w') as f: + f.write(self.key_str) + response = requests.post(url=self.url, data=data, cert=(cert_file_path, key_file_path)) + return response + + def _should_retry_token_retrieval_params( + self, attempt: int, + response: Optional[requests.Response] = None, + status_code: Optional[int] = None, + exp: Optional[Exception] = None + ) -> dict: + """ + Determines whether to retry the token retrieval operation based on the HTTP status code, + number of attempts, or specific exceptions, and calculates the delay before the next retry. + + :param attempt: Current attempt number for the token retrieval. + :type attempt: int + :param response: HTTP response object related to the token retrieval, if available. + :type response: Optional[requests.Response] + :param status_code: HTTP status code from the token retrieval response, if available. + :type status_code: Optional[int] + :param exp: Exception raised during the token retrieval, if any. + :type exp: Optional[Exception] + + :return: A dictionary containing: + - ``should_retry`` (bool): Indicates whether the operation should be retried. + - ``delay`` (float): The delay before the next retry attempt, if applicable. + :rtype: dict + """ + + if status_code in [401, 403] or attempt == MAX_RETRY_ATTEMPTS_FOR_TOKEN: + return {"should_retry": False, + "delay": 0} + + if status_code in [408, 502, 504] or (exp and isinstance(exp, ConnectionError)): + return {"should_retry": True, + "delay": BASE_DELAY_FOR_TOKEN_RETRY * (2 ** attempt)} + + if status_code in [500, 503]: + return {"should_retry": True, + "delay": BASE_DELAY_FOR_TOKEN_RETRY * (3 ** attempt)} + + if status_code == 429: + return {"should_retry": True, + "delay": float(response.headers.get("Retry-After", 1))} + + return {"should_retry": False, + "delay": 0} + + def _execute_token_request(self) -> requests.Response: + """Executes a request to the xsuaa server to retrieve the token. + + :return: The response from the xsuaa server. + :rtype: requests.Response + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorException` if an unexpected exception occurs""" + data = {"grant_type": "client_credentials", "client_id": self.client_id} + if self.client_secret: + data["client_secret"] = self.client_secret + return requests.post(url=self.url, data=data) + if self.cert_str and self.key_str: + return self._request_token_with_cert_key_str(data) + if self.cert_file_path and self.key_file_path: + return requests.post(url=self.url, data=data, cert=(self.cert_file_path, self.key_file_path)) + raise AIAPIAuthenticatorException(error_message=PARAM_ERROR_MESSAGE) + + def _sleep_before_retry(self, attempt: int, *, response: Optional[requests.Response] = None, + status_code: Optional[int] = None, exp: Optional[Exception] = None) -> bool: + params = self._should_retry_token_retrieval_params( + attempt=attempt, + response=response, + status_code=status_code, + exp=exp, + ) + if not params["should_retry"]: + return False + time.sleep(params["delay"]) + return True + + def _retrieve_token_with_retries(self) -> tuple[requests.Response, int, Optional[str]]: + """Retrieves the token from the xsuaa server with retries. + + :return: A tuple containing: response, status_code, error_msg + :rtype: tuple[requests.Response, int, Optional[str]] + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorException` if an unexpected exception occurs + """ + attempt = 0 + error_msg: Optional[str] = None + status_code = 0 + + while status_code // 100 != 2: + try: + response = self._execute_token_request() + status_code = response.status_code + error_msg = response.text + except AIAPIAuthenticatorException: + raise + except Exception as exception: # pylint:disable=broad-except + if self._sleep_before_retry(attempt, exp=exception): + attempt += 1 + continue + raise AIAPIAuthenticatorException(status_code=500, error_message=error_msg) from exception + + if self._sleep_before_retry(attempt, response=response, status_code=status_code): + attempt += 1 + continue + + return response, status_code, error_msg + + return response, status_code, error_msg + + def _raise_for_status(self, status_code: int, error_msg: Optional[str]) -> None: + """Raises an exception based on the HTTP status code received from the xsuaa server. + :param status_code: The HTTP status code. + :type status_code: int + :param error_msg: The error message. + :type error_msg: Optional[str] + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorInvalidRequestException` if the status code is 400 + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorAuthorizationException` if the status code is 401 + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorForbiddenException` if the status code is 403 + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorMethodNotAllowedException` if the status code is 405 + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorTimeoutException` if the status code is 408 + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorServerException` if the status code is not 2XX + """ + if status_code == 400: + raise AIAPIAuthenticatorInvalidRequestException(error_message=error_msg) + elif status_code == 401: + raise AIAPIAuthenticatorAuthorizationException(error_message=error_msg) + elif status_code == 403: + raise AIAPIAuthenticatorForbiddenException(error_message=error_msg) + elif status_code == 405: + raise AIAPIAuthenticatorMethodNotAllowedException(error_message=error_msg) + elif status_code == 408: + raise AIAPIAuthenticatorTimeoutException(error_message=error_msg) + elif status_code // 100 != 2: + raise AIAPIAuthenticatorServerException(status_code=status_code, error_message=error_msg) + + def _update_token_from_response(self, response: requests.Response, error_msg: Optional[str]) -> None: + """Updates the token from the response received from the xsuaa server. + :param response: The response received from the xsuaa server. + :type response: requests.Response + :param error_msg: The error message received from the xsuaa server. + :type error_msg: Optional[str] + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorException` if an unexpected exception occurs while + """ + try: + payload = response.json() + access_token = payload["access_token"] + self.token = f"Bearer {access_token}" + self._calc_token_expiry_date(payload["expires_in"]) + except Exception as exception: # pylint:disable=broad-except + raise AIAPIAuthenticatorException(status_code=500, error_message=error_msg) from exception + + def get_token(self) -> str: + """Retrieves the token from the xsuaa server or from cache when expiration date not reached. + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorException` if an unexpected exception occurs while + trying to retrieve the token + :return: The Bearer token + :rtype: str + """ + with self.lock: # Thread-Safe + if self._should_refresh_token(): + response, status_code, error_msg = self._retrieve_token_with_retries() + self._raise_for_status(status_code, error_msg) + self._update_token_from_response(response, error_msg) + return self.token + + def _should_refresh_token(self): + if self.token is None or self.token_expiry_date is None: + return True + + now = datetime.now(timezone.utc) + # Check if token has expired incl. buffer + return self.token_expiry_date - now < timedelta(minutes=60) + + def _calc_token_expiry_date(self, expires_in: str): + now = datetime.now(timezone.utc) + # Calculate the token expiry date starting now adding expires in + self.token_expiry_date = now + timedelta(seconds=int(expires_in)) diff --git a/packages/base/ai_api_client_sdk/helpers/constants.py b/packages/base/ai_api_client_sdk/helpers/constants.py new file mode 100644 index 0000000..8067d91 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/constants.py @@ -0,0 +1,13 @@ +import re + +from enum import Enum + +SCENARIO_LABEL_NAME_PATTERN = re.compile(r"^scenarios\.ai\.sap\.com/[\w.-]+$") + +DEBUG_ENV_VAR_NAME = "DEBUG" +SKIP_AUTH_ENV_VAR = 'SKIP_AUTHORIZATION' + +class Timeouts(Enum): + READ_TIMEOUT = 60 + CONNECT_TIMEOUT = 60 + NUM_REQUEST_RETRIES = 3 diff --git a/packages/base/ai_api_client_sdk/helpers/datetime_parser.py b/packages/base/ai_api_client_sdk/helpers/datetime_parser.py new file mode 100644 index 0000000..2bfefb7 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/datetime_parser.py @@ -0,0 +1,27 @@ +from datetime import datetime, timezone + +DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +DATETIME_FORMAT_FLOAT = '%Y-%m-%dT%H:%M:%S.%fZ' +DATETIME_FORMAT_36 = "%Y-%m-%dT%H:%M:%S+00:00" +DATETIME_FORMAT_FLOAT_TZ = '%Y-%m-%dT%H:%M:%S.%f+00:00' + + +def parse_datetime(datetime_str: str) -> datetime: + try: + parsed_datetime = datetime.strptime(datetime_str, DATETIME_FORMAT_36) + except ValueError as _: + try: + parsed_datetime = datetime.strptime(datetime_str, DATETIME_FORMAT) + except ValueError as _: + try: + parsed_datetime = datetime.strptime(datetime_str, DATETIME_FORMAT_FLOAT) + except ValueError as _: + if len(datetime_str) > 32: + datetime_str = f'{datetime_str[:26]}{datetime_str[29:]}' + parsed_datetime = datetime.strptime(datetime_str, DATETIME_FORMAT_FLOAT_TZ) + if not parsed_datetime.tzinfo: + parsed_datetime = datetime(year=parsed_datetime.year, month=parsed_datetime.month, day=parsed_datetime.day, + hour=parsed_datetime.hour, minute=parsed_datetime.minute, + second=parsed_datetime.second, microsecond=parsed_datetime.microsecond, + tzinfo=timezone.utc) + return parsed_datetime diff --git a/packages/base/ai_api_client_sdk/helpers/llm_helper.py b/packages/base/ai_api_client_sdk/helpers/llm_helper.py new file mode 100644 index 0000000..56b3b55 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/llm_helper.py @@ -0,0 +1,43 @@ +from functools import partial + +from ai_api_client_sdk.helpers.constants import SCENARIO_LABEL_NAME_PATTERN + + +def get_attr(obj, attr): + if isinstance(obj, dict): + return obj.get(attr) + else: + return getattr(obj, attr, None) + + +class _NamedPartial(partial): + def __repr__(self): + return f"functools.partial(get_attr, attr={self.keywords['attr']!r})" + + +get_labels = _NamedPartial(get_attr, attr='labels') +get_key = _NamedPartial(get_attr, attr='key') +get_value = _NamedPartial(get_attr, attr='value') + + +def check_if_llm_scenario(scenario): + labels = get_labels(scenario) + if not labels: + return False + for label in labels: + key = get_key(label) + value = get_value(label) + if SCENARIO_LABEL_NAME_PATTERN.match(key) and value: + return True + return False + + +def filter_for_llm_scenarios(response_dict): + res_dict = {} + filtered_scenarios = [] + for scenario in response_dict['resources']: + if check_if_llm_scenario(scenario=scenario): + filtered_scenarios.append(scenario) + res_dict['count'] = len(filtered_scenarios) + res_dict['resources'] = filtered_scenarios + return res_dict diff --git a/packages/base/ai_api_client_sdk/helpers/logging.py b/packages/base/ai_api_client_sdk/helpers/logging.py new file mode 100644 index 0000000..c85bcef --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/logging.py @@ -0,0 +1,17 @@ +import logging +import os + +from ai_api_client_sdk.helpers.constants import DEBUG_ENV_VAR_NAME + +DEFAULT_LOG_LEVEL = logging.INFO +LOGGER_NAME = "ai-api-client-sdk" + +def get_logger(): + return logging.getLogger(get_logger_name()) + +def get_logger_name(): + return LOGGER_NAME + +def set_log_level(logger: logging.Logger): + debug = os.getenv(DEBUG_ENV_VAR_NAME) is not None and os.getenv(DEBUG_ENV_VAR_NAME).lower() == 'true' + logger.setLevel(logging.DEBUG if debug else DEFAULT_LOG_LEVEL) diff --git a/packages/base/ai_api_client_sdk/helpers/rest_client.py b/packages/base/ai_api_client_sdk/helpers/rest_client.py new file mode 100644 index 0000000..1a85821 --- /dev/null +++ b/packages/base/ai_api_client_sdk/helpers/rest_client.py @@ -0,0 +1,265 @@ +import json +import os +from typing import Callable, Dict, Union + +import humps +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + +from ai_api_client_sdk.exception import AIAPIAuthorizationException, AIAPIInvalidRequestException, \ + AIAPINotFoundException, AIAPIPreconditionFailedException, AIAPIServerException +from ai_api_client_sdk.helpers.constants import SKIP_AUTH_ENV_VAR, Timeouts +from ai_api_client_sdk.helpers.logging import get_logger, set_log_level + + +class RestClient: + """RestClient is the class implemented for sending the requests to the server. + + :param base_url: Base URL of the server. Should include the base path as well. (i.e., "/scenarios" should + work) + :type base_url: str + :param get_token: the function which returns the Bearer token, when called + :type get_token: Callable[[], str] + :param resource_group: The default resource group which will be used while sending the requests to the server, + defaults to None + :type resource_group: str + :param client_type: Used for Metering to distinguish eg AI Launchpad python SDKs etc, + defaults to None + :type client_type: str + :param read_timeout: Read timeout for requests in seconds, defaults to 60s + :type read_timeout: int + :param connect_timeout: Connect timeout for requests in seconds, defaults to 60s + :type connect_timeout: int + :param num_request_retries: Number of retries for failing requests with http status code 429, 500, 502, 503 or 504, + defaults to 60s + :type num_request_retries: int + """ + + logger = get_logger() + + def __init__(self, base_url: str, get_token: Callable[[], str], resource_group: str = None, client_type: str = None, + read_timeout=Timeouts.READ_TIMEOUT.value, connect_timeout=Timeouts.CONNECT_TIMEOUT.value, + num_request_retries=Timeouts.NUM_REQUEST_RETRIES.value): + self.base_url: str = base_url + self.get_token: Callable[[], str] = get_token + self.resource_group_header: str = 'AI-Resource-Group' + self.client_type_header: str = 'AI-Client-Type' + self.headers: dict = {} + if resource_group: + self.headers[self.resource_group_header] = resource_group + # Priority: Environment variable > parameter > None + client_type = os.environ.get('AI_CLIENT_TYPE', client_type) + if client_type: + self.headers[self.client_type_header] = client_type + self.read_timeout = read_timeout + self.connect_timeout = connect_timeout + self.num_request_retries = num_request_retries + + def _handle_request(self, method: str, path: str, params: Dict[str, str] = None, + body_json: Dict[str, Union[str, dict]] = None, headers: Dict[str, str] = None, + resource_group: str = None, return_bytes_content: bool = False, + convert_body_to_camel_case: bool = True, convert_params_to_camel_case: bool = True, + **kwargs) -> dict: + error_description = f'Failed to {method.lower()} {path}' + set_log_level(self.logger) + requests_session = requests.Session() + + retries = Retry(total=self.num_request_retries, + read=self.num_request_retries, + connect=self.num_request_retries, + status=self.num_request_retries, + backoff_factor=0.1, + status_forcelist=[429, 500, 502, 503, 504]) + + requests_session.mount('http://', HTTPAdapter(max_retries=retries)) + requests_session.mount('https://', HTTPAdapter(max_retries=retries)) + + requests_function = getattr(requests_session, method) + url = f'{self.base_url}{path}' + headers = headers or {} + headers.update(self.headers.copy()) + if os.environ.get(SKIP_AUTH_ENV_VAR, '').lower() != 'true': + headers['Authorization'] = self.get_token() + if resource_group: + headers[self.resource_group_header] = resource_group + if body_json and convert_body_to_camel_case: + body_json = humps.camelize(body_json) + if params and convert_params_to_camel_case: + params = humps.camelize(params) + + headers_for_log = headers.copy() + headers_for_log['Authorization'] = '***' + self.logger.debug(f"Sending {method} request to {url} with headers: {headers_for_log}, params: {params}" + f", payload: {body_json}.") + + response = requests_function(url=url, params=params, json=body_json, headers=headers, + timeout=(self.connect_timeout, self.read_timeout), **kwargs) + self.logger.debug(f"Received response from {url} with status code: {response.status_code}, " + f"response: {response.text}.") + + if response.status_code == 401: + raise AIAPIAuthorizationException(description=error_description, error_message=response.text) + + try: + response_json = response.json() + except json.decoder.JSONDecodeError: + response_json = response.text + + if type(response_json) is dict and 'error' in response_json: + self.raise_ai_api_exception(error_description, response, response_json) + elif response.status_code // 100 != 2: + raise AIAPIServerException(description=error_description, error_message=response.text, + status_code=response.status_code) + if return_bytes_content: + return response.content + else: + return humps.decamelize(response_json) + + @staticmethod + def raise_ai_api_exception(error_description, response, response_json): + status_code = response.status_code + error_message = response_json['error']['message'] + error_code = response_json['error']['code'] + request_id = response_json['error'].get('requestId') + error_details = response_json['error'].get('details') + if status_code == 400: + raise AIAPIInvalidRequestException(description=error_description, error_message=error_message, + error_code=error_code, request_id=request_id, details=error_details) + elif status_code == 404: + raise AIAPINotFoundException(description=error_description, error_message=error_message, + error_code=error_code, request_id=request_id, details=error_details) + elif status_code == 412: + raise AIAPIPreconditionFailedException(description=error_description, error_message=error_message, + error_code=error_code, request_id=request_id, + details=error_details) + else: + raise AIAPIServerException(status_code=status_code, description=error_description, + error_message=error_message, error_code=error_code, request_id=request_id, + details=error_details) + + def post(self, path: str, body: Dict[str, Union[str, dict]] = None, headers: Dict[str, str] = None, + resource_group: str = None, **kwargs) -> dict: + """Sends a POST request to the server. + + :param path: path of the endpoint the request should be sent to + :type path: str + :param body: body of the request, defaults to None + :type body: Dict[str, str], optional + :param headers: headers of the request, defaults to None + :type headers: Dict[str, str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this, or the + resource_group property of this class should be set. + :type resource_group: str + :param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc. + :type kwargs: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The JSON response from the server (The keys decamelized) + :rtype: dict + """ + return self._handle_request('post', path=path, body_json=body, headers=headers, + resource_group=resource_group, **kwargs) + + def get(self, path: str, params: Dict[str, str] = None, headers: Dict[str, str] = None, + resource_group: str = None, return_bytes_content: bool = False, **kwargs) -> Union[dict, int]: + """Sends a GET request to the server. + + :param path: path of the endpoint the request should be sent to + :type path: str + :param params: parameters of the request, defaults to None + :type params: Dict[str, str], optional + :param headers: headers of the request, defaults to None + :type headers: Dict[str, str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this, or the + resource_group property of this class should be set. + :type resource_group: str + :param return_bytes_content: expected response.content is bytes + :type return_bytes_content: bool + :param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc. + :type kwargs: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The JSON response from the server (The keys decamelized) + :rtype: Union[dict, int] + """ + return self._handle_request('get', path=path, params=params, headers=headers, + resource_group=resource_group, return_bytes_content=return_bytes_content, **kwargs) + + def patch(self, path: str, body: Dict[str, Union[str, dict, list]], headers: Dict[str, str] = None, + resource_group: str = None, **kwargs) -> dict: + """Sends a PATCH request to the server. + + :param path: path of the endpoint the request should be sent to + :type path: str + :param body: body of the request + :type body: Dict[str, Union[str, dict, list]] + :param headers: headers of the request, defaults to None + :type headers: Dict[str, str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this, or the + resource_group property of this class should be set. + :type resource_group: str + :param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc. + :type kwargs: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The JSON response from the server (The keys decamelized) + :rtype: dict + """ + return self._handle_request('patch', path=path, body_json=body, headers=headers, + resource_group=resource_group, **kwargs) + + def delete(self, path: str, params: Dict[str, str] = None, headers: Dict[str, str] = None, + resource_group: str = None, **kwargs) -> dict: + """Sends a DELETE request to the server. + + :param path: path of the endpoint the request should be sent to + :type path: str + :param params: parameters of the request, defaults to None + :type params: Dict[str, str], optional + :param headers: headers of the request, defaults to None + :type headers: Dict[str, str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this, or the + resource_group property of this class should be set. + :type resource_group: str + :param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc. + :type kwargs: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The JSON response from the server (The keys decamelized) + :rtype: dict + """ + return self._handle_request('delete', path=path, params=params, headers=headers, + resource_group=resource_group, **kwargs) diff --git a/packages/base/ai_api_client_sdk/models/__init__.py b/packages/base/ai_api_client_sdk/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/ai_api_client_sdk/models/ai_api_capabilities.py b/packages/base/ai_api_client_sdk/models/ai_api_capabilities.py new file mode 100644 index 0000000..3aad593 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_capabilities.py @@ -0,0 +1,71 @@ +from typing import Any, Dict + +from ai_api_client_sdk.models.ai_api_capabilities_bulk_updates import AIAPICapabilitiesBulkUpdates +from ai_api_client_sdk.models.ai_api_capabilities_logs import AIAPICapabilitiesLogs + + +class AIAPICapabilities: + """The AIAPICapabilities object represent the capabilities of the AI API + + :param multitenant: indicates whether resource groups are supported, defaults to True + :type multitenant: bool, optional + :param shareable: indicates whether clients can share an instance, defaults to True + :type shareable: bool, optional + :param static_deployments: indicates whether the static, always running deployments are supported, defaults to True + :type static_deployments: bool, optional + :param user_deployments: indicates whether deployment creation by users are supported, defaults to True + :type user_deployments: bool, optional + :param time_to_live_deployments: indicate whether ttl value of deployment are supported, defaults to False + :type time_to_live_deployments: bool, optional + :param user_executions: indicates whether execution creation by users are supported, defaults to True + :type user_executions: bool, optional + :param bulk_updates: An object, defining the bulk updates capabilities, defaults to None + :type bulk_updates: class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates`, + optional + :param execution_schedules: indicates whether execution schedules are supported, defaults to False + :type execution_schedules: bool, optional + :param logs: An object, defining the logs capabilities, defaults to None + :type logs: class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, multitenant: bool = True, shareable: bool = True, static_deployments: bool = True, + user_deployments: bool = True, time_to_live_deployments: bool = False, user_executions: bool = True, + bulk_updates: AIAPICapabilitiesBulkUpdates = None, execution_schedules: bool = False, + logs: AIAPICapabilitiesLogs = None, **kwargs): + self.multitenant: bool = multitenant + self.shareable: bool = shareable + self.static_deployments: bool = static_deployments + self.user_deployments: bool = user_deployments + self.time_to_live_deployments: bool = time_to_live_deployments + self.user_executions: bool = user_executions + self.bulk_updates: AIAPICapabilitiesBulkUpdates = bulk_updates + self.execution_schedules: bool = execution_schedules + self.logs: AIAPICapabilitiesLogs = logs + + @staticmethod + def from_dict(ai_api_capabilities_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities` object, created from the + values in the dict provided as parameter + + :param ai_api_capabilities_dict: Dict which includes the necessary values to create the object + :type ai_api_capabilities_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities` + """ + if 'logs' in ai_api_capabilities_dict: + ai_api_capabilities_dict['logs'] = AIAPICapabilitiesLogs.from_dict(ai_api_capabilities_dict['logs']) + if 'bulk_updates' in ai_api_capabilities_dict: + ai_api_capabilities_dict['bulk_updates'] = AIAPICapabilitiesBulkUpdates.from_dict(ai_api_capabilities_dict['bulk_updates']) + return AIAPICapabilities(**ai_api_capabilities_dict) + + def __eq__(self, other): + if not isinstance(other, AIAPICapabilities): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_capabilities_bulk_updates.py b/packages/base/ai_api_client_sdk/models/ai_api_capabilities_bulk_updates.py new file mode 100644 index 0000000..f27877e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_capabilities_bulk_updates.py @@ -0,0 +1,38 @@ +from typing import Dict + + +class AIAPICapabilitiesBulkUpdates: + """The AIAPICapabilitiesBulkUpdates object represents the bulk updates capabilities + + :param deployments: indicates whether bulk updates for executions are supported, defaults to False + :type deployments: bool, optional + :param executions: indicates whether bulk updates for executions are supported, defaults to False + :type executions: bool, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, executions: bool = False, deployments: bool = False, **kwargs): + self.executions: bool = executions + self.deployments: bool = deployments + + @staticmethod + def from_dict(ai_api_capabilities_bulk_updates_dict: Dict[str, bool]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates` object, created + from the values in the dict provided as parameter + + :param ai_api_capabilities_bulk_updates_dict: Dict which includes the necessary values to create the object + :type ai_api_capabilities_bulk_updates_dict: Dict[str, bool] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates` + """ + return AIAPICapabilitiesBulkUpdates(**ai_api_capabilities_bulk_updates_dict) + + def __eq__(self, other): + if not isinstance(other, AIAPICapabilitiesBulkUpdates): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_capabilities_logs.py b/packages/base/ai_api_client_sdk/models/ai_api_capabilities_logs.py new file mode 100644 index 0000000..e5289e1 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_capabilities_logs.py @@ -0,0 +1,38 @@ +from typing import Dict + + +class AIAPICapabilitiesLogs: + """The AIAPICapabilitiesLogs object represents the log capabilities + + :param user_executions: indicates whether logs for executions are supported, defaults to True + :type user_executions: bool, optional + :param user_deployments: indicates whether logs for deployments are supported, defaults to True + :type user_deployments: bool, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, executions: bool = True, deployments: bool = True, **kwargs): + self.executions: bool = executions + self.deployments: bool = deployments + + @staticmethod + def from_dict(ai_api_capabilities_logs_dict: Dict[str, bool]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs` object, created + from the values in the dict provided as parameter + + :param ai_api_capabilities_logs_dict: Dict which includes the necessary values to create the object + :type ai_api_capabilities_logs_dict: Dict[str, bool] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs` + """ + return AIAPICapabilitiesLogs(**ai_api_capabilities_logs_dict) + + def __eq__(self, other): + if not isinstance(other, AIAPICapabilitiesLogs): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_limits.py b/packages/base/ai_api_client_sdk/models/ai_api_limits.py new file mode 100644 index 0000000..2deac0c --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_limits.py @@ -0,0 +1,45 @@ +from typing import Dict, Any + +from ai_api_client_sdk.models.ai_api_limits_deployments import AIAPILimitsDeployments +from ai_api_client_sdk.models.ai_api_limits_executions import AIAPILimitsExecutions + + +class AIAPILimits: + """The AIAPILimits object represents the the limits for executions and deployments + + :param executions: represents the limits for executions, defaults to None + :type executions: class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions`, optional + :param deployments: represents the limits for deployments, defaults to None + :type deployments: class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, executions: AIAPILimitsExecutions = None, deployments: AIAPILimitsDeployments = None, **kwargs): + self.executions: AIAPILimitsExecutions = executions + self.deployments: AIAPILimitsDeployments = deployments + + @staticmethod + def from_dict(ai_api_limits_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimits` object, created from the values in the + dict provided as parameter + + :param ai_api_limits_dict: Dict which includes the necessary values to create the object + :type ai_api_limits_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimits` + """ + if 'executions' in ai_api_limits_dict: + ai_api_limits_dict['executions'] = AIAPILimitsExecutions.from_dict(ai_api_limits_dict['executions']) + if 'deployments' in ai_api_limits_dict: + ai_api_limits_dict['deployments'] = AIAPILimitsDeployments.from_dict(ai_api_limits_dict['deployments']) + return AIAPILimits(**ai_api_limits_dict) + + def __eq__(self, other): + if not isinstance(other, AIAPILimits): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_limits_deployments.py b/packages/base/ai_api_client_sdk/models/ai_api_limits_deployments.py new file mode 100644 index 0000000..c64942b --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_limits_deployments.py @@ -0,0 +1,29 @@ +from typing import Dict + +from ai_api_client_sdk.models.ai_api_limits_enactments import AIAPILimitsEnactments + + +class AIAPILimitsDeployments(AIAPILimitsEnactments): + """The AIAPILimitsDeployments object represents the the limits for deployments + + :param max_running_count: max number of deployments per resource group, <0 means unlimited, defaults to -1 + :type max_running_count: int, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, max_running_count: int = -1, **kwargs): + super().__init__(max_running_count=max_running_count, **kwargs) + + @staticmethod + def from_dict(ai_api_limits_deployments_dict: Dict[str, int]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments` object, created + from the values in the dict provided as parameter + + :param ai_api_limits_deployments_dict: Dict which includes the necessary values to create the object + :type ai_api_limits_deployments_dict: Dict[str, int] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments` + """ + return AIAPILimitsDeployments(**ai_api_limits_deployments_dict) + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_limits_enactments.py b/packages/base/ai_api_client_sdk/models/ai_api_limits_enactments.py new file mode 100644 index 0000000..a0b2b19 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_limits_enactments.py @@ -0,0 +1,21 @@ +class AIAPILimitsEnactments: + """The AIAPILimitsEnactments object represents the the limits for enactments (common for both executions + and deployments) + + :param max_running_count: max number of enactments per resource group, <0 means unlimited, defaults to -1 + :type max_running_count: int, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, max_running_count: int = -1, **kwargs): + self.max_running_count: int = max_running_count + + def __eq__(self, other): + if not isinstance(other, AIAPILimitsEnactments): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_limits_executions.py b/packages/base/ai_api_client_sdk/models/ai_api_limits_executions.py new file mode 100644 index 0000000..171c71e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_limits_executions.py @@ -0,0 +1,29 @@ +from typing import Dict + +from ai_api_client_sdk.models.ai_api_limits_enactments import AIAPILimitsEnactments + + +class AIAPILimitsExecutions(AIAPILimitsEnactments): + """The AIAPILimitsExecutions object represents the the limits for executions + + :param max_running_count: max number of executions per resource group, <0 means unlimited, defaults to -1 + :type max_running_count: int, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, max_running_count: int = -1, **kwargs): + super().__init__(max_running_count=max_running_count, **kwargs) + + @staticmethod + def from_dict(ai_api_limits_executions_dict: Dict[str, int]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions` object, created + from the values in the dict provided as parameter + + :param ai_api_limits_executions_dict: Dict which includes the necessary values to create the object + :type ai_api_limits_executions_dict: Dict[str, int] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions` + """ + return AIAPILimitsExecutions(**ai_api_limits_executions_dict) + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/ai_api_meta.py b/packages/base/ai_api_client_sdk/models/ai_api_meta.py new file mode 100644 index 0000000..52c1c9f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/ai_api_meta.py @@ -0,0 +1,48 @@ +from typing import Any, Dict + +from ai_api_client_sdk.models.ai_api_capabilities import AIAPICapabilities +from ai_api_client_sdk.models.ai_api_limits import AIAPILimits + + +class AIAPIMeta: + """The AIAPIMeta object represents the metadata and capabilities of the AI API + + :param version: version of the AI API + :type version: str + :param capabilities: capabilities of AI API, defaults to None + :type capabilities: class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities` + :param limits: limits of AI API, defaults to None + :type limits: class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimits`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, version: str, capabilities: AIAPICapabilities = None, limits: AIAPILimits = None, **kwargs): + self.version: str = version + self.capabilities: AIAPICapabilities = capabilities + self.limits: AIAPILimits = limits + + @staticmethod + def from_dict(ai_api_meta_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMeta` object, created + from the values in the dict provided as parameter + + :param ai_api_meta_dict: Dict which includes the necessary values to create the object + :type ai_api_meta_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMeta` + """ + if 'capabilities' in ai_api_meta_dict: + ai_api_meta_dict['capabilities'] = AIAPICapabilities.from_dict(ai_api_meta_dict['capabilities']) + if 'limits' in ai_api_meta_dict: + ai_api_meta_dict['limits'] = AIAPILimits.from_dict(ai_api_meta_dict['limits']) + return AIAPIMeta(**ai_api_meta_dict) + + def __eq__(self, other): + if not isinstance(other, AIAPIMeta): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/api_version.py b/packages/base/ai_api_client_sdk/models/api_version.py new file mode 100644 index 0000000..bc47644 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/api_version.py @@ -0,0 +1,41 @@ +from typing import Dict + + +class APIVersion: + """The APIVersion object represents the description of an API version + + :param version_id: API version identifier, defaults to None + :type version_id: str, optional + :param url: URL of the API version, defaults to None + :type url: str, optional + :param description: API version description + :type description: str, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, version_id: str = None, url: str = None, description: str = None, **kwargs): + self.version_id: str = version_id + self.url: str = url + self.description: str = description + + @staticmethod + def from_dict(api_version_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.api_version.APIVersion` object, created from the + values in the dict provided as parameter + + :param api_version_dict: Dict which includes the necessary values to create the object + :type api_version_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.api_version.APIVersion` + """ + return APIVersion(**api_version_dict) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return f"Version ID: {self.version_id}, URL: {self.url}" diff --git a/packages/base/ai_api_client_sdk/models/artifact.py b/packages/base/ai_api_client_sdk/models/artifact.py new file mode 100644 index 0000000..6793282 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/artifact.py @@ -0,0 +1,90 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.scenario import Scenario + + +class Artifact: + """The Artifact object defines an artifact + + :param name: Name of the artifact + :type name: str + :param id: ID of the artifact + :type id: str + :param url: URL of the artifact + :type url: str + :param kind: Kind of the artifact + :type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind` + :param scenario_id: ID of the scenario which the artifact belongs to + :type scenario_id: str + :param created_at: Time when the artifact was created + :type created_at: datetime + :param modified_at: Time when the artifact was last modified + :type modified_at: datetime + :param execution_id: ID of the execution which the artifact resulted from, defaults to None + :type execution_id: str, optional + :param configuration_id: ID of the configuration which the artifact relates to, defaults to None + :type configuration_id: str, optional + :param description: Description of the artifact, defaults to None + :type description: str, optional + :param labels: List of the labels of the artifact, defaults to None + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + :param scenario: A dict, which gives detailed information on scenario, defaults to None + :type scenario: class:`ai_api_client_sdk.models.scenario.Scenario`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + class Kind(Enum): + MODEL = 'model' + DATASET = 'dataset' + RESULTSET = 'resultset' + OTHER = 'other' + + def __init__(self, name: str, id: str, url: str, kind: Kind, scenario_id: str, created_at: datetime, + modified_at: datetime, execution_id: str = None, configuration_id: str = None, description: str = None, + labels: List[Label] = None, scenario:Scenario = None, **kwargs): + self.id: str = id + self.name: str = name + self.url: str = url + self.kind: Artifact.Kind = kind # pylint: disable=used-before-assignment + self.description: str = description + self.scenario_id: str = scenario_id + self.execution_id: str = execution_id + self.configuration_id: str = configuration_id + self.labels: List[Label] = labels + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + self.scenario: Scenario = scenario + + def __eq__(self, other): + if not isinstance(other, Artifact): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return "Artifact id: " + str(self.id) + ", Artifact description: " + str(self.description) + + @staticmethod + def from_dict(artifact_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.artifact.Artifact` object, created from the values in the dict + provided as parameter + + :param artifact_dict: Dict which includes the necessary values to create the object + :type artifact_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.artifact.Artifact` + """ + artifact_dict['kind'] = Artifact.Kind(artifact_dict['kind']) + artifact_dict['created_at'] = parse_datetime(artifact_dict['created_at']) + artifact_dict['modified_at'] = parse_datetime(artifact_dict['modified_at']) + if artifact_dict.get('labels'): + artifact_dict['labels'] = [Label.from_dict(l) for l in artifact_dict['labels']] + if artifact_dict.get('scenario'): + artifact_dict['scenario'] = Scenario.from_dict(artifact_dict['scenario']) + return Artifact(**artifact_dict) diff --git a/packages/base/ai_api_client_sdk/models/artifact_create_response.py b/packages/base/ai_api_client_sdk/models/artifact_create_response.py new file mode 100644 index 0000000..b679c7a --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/artifact_create_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict + +from .base_models import BasicResponse + + +class ArtifactCreateResponse(BasicResponse): + """The ArtifactCreateResponse object defines the response of the artifact create request + :param id: ID of the artifact + :type id: str + :param message: Response message from the server + :type message: str + :param url: URL of the artifact + :type url: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, message: str, url: str, **kwargs): + super().__init__(id=id, message=message, **kwargs) + self.url: str = url + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse` + """ + return ArtifactCreateResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/artifact_query_response.py b/packages/base/ai_api_client_sdk/models/artifact_query_response.py new file mode 100644 index 0000000..2bfcfab --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/artifact_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .artifact import Artifact +from .base_models import QueryResponse + + +class ArtifactQueryResponse(QueryResponse): + """The ArtifactQueryResponse object defines the response of the artifact query request + :param resources: List of the artifacts returned from the server + :type resources: List[class:`ai_api_client_sdk.models.artifact.Artifact`] + :param count: Total number of the queried artifacts + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Artifact], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse` + """ + response_dict['resources'] = [Artifact.from_dict(r) for r in response_dict['resources']] + return ArtifactQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/base_models.py b/packages/base/ai_api_client_sdk/models/base_models.py new file mode 100644 index 0000000..f19e6f3 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/base_models.py @@ -0,0 +1,222 @@ +from enum import Enum +from typing import Dict, Union + +from aenum import Enum as AEnum + +from ai_api_client_sdk.models.target_status import TargetStatus + + +class KeyValue: + """KeyValue object defines a key-value pair + + :param key: key of the pair + :type key: str + :param value: value of the pair + :type value: str + """ + + def __init__(self, key: str, value: str, **kwargs): + self.key: str = key + self.value: str = value + + def to_dict(self): + return {'key': self.key, 'value': self.value} + + def __eq__(self, other): + if not isinstance(other, KeyValue): + return False + return self.key == other.key and self.value == other.value + + def __str__(self): + return "Key: " + str(self.key) + ", Value: " + str(self.value) + + +class NameValue: + """KeyValue object defines a name-value pair + + :param name: name of the pair + :type name: str + :param value: value of the pair + :type value: str + """ + + def __init__(self, name: str, value: str, **kwargs): + self.name: str = name + self.value: str = value + + def __eq__(self, other): + if not isinstance(other, NameValue): + return False + return self.name == other.name and self.value == other.value + + def __str__(self): + return "Name: " + str(self.name) + ", Value: " + str(self.value) + + +class Name: + """KeyValue object defines a name + + :param name: name + :type name: str + """ + + def __init__(self, name: str, **kwargs): + self.name: str = name + + def __eq__(self, other): + if not isinstance(other, Name): + return False + return self.name == other.name + + def __str__(self): + return "Name: " + str(self.name) + + +class QueryResponse: + """The QueryResponse object defines the response from the server to a query request + + :param resources: List of the resources returned from the server + :type resources: list + :param count: Total number of the queried resources + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, resources: list, count: int, **kwargs): + self.resources: list = resources + self.count: int = count + + def __str__(self): + return "Resources: [" + ", ".join(["{" + str(r) + "}" for r in self.resources]) + \ + "], Count: " + str(self.count) + + +class BasicResponse: + """The BasicResponse object defines the response from the server + + :param id: ID of the relevant resource + :type id: str + :param message: Response message from the server + :type message: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, id: str, message: str, **kwargs): + self.id: str = id + self.message: str = message + + def __str__(self): + return "Id: " + str(self.id) + ", Message: " + str(self.message) + + @staticmethod + def from_dict(br_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.base_models.BasicResponse` object, created from the values in the + dict provided as parameter + + :param br_dict: Dict which includes the necessary values to create the object + :type br_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + return BasicResponse(**br_dict) + + +class BasicErrorResponse: + """The BasicErrorResponse object defines the response from the server + + :param error_code: Error code from the server + :type error_code: str + :param message: Error message from the server + :type message: str + :param request_id: Request ID + :type request_id: str + :param target: target + :type target: str, optional + :param details: Error details + :type details: Dict, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, code: str, message: str, request_id: str, target: str, details: Dict = None, **kwargs): + self.code: str = code + self.message: str = message + self.request_id: str = request_id + self.target: str = target + self.details: Dict = details + + def __str__(self): + return "Error code: " + str(self.code) + ", Error Message: " + str(self.message) + + @staticmethod + def from_dict(ber_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.base_models.BasicErrorResponse` object, created from the values + provided as parameter + + :param ber_dict: Dict which includes the necessary values to create the object + :type ber_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.base_models.BasicErrorResponse` + """ + return BasicErrorResponse(**ber_dict) + + +class BasicModifyRequest: + """The BasicModifyRequest object defines the request from client + :param id: ID of the relevant resource + :type id: str + :param target_status: Target Status of the resource + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus` + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, target_status: TargetStatus, **kwargs): + self.id: str = id + self.target_status: TargetStatus = target_status + + def to_dict(self): + return {'id': self.id, 'target_status': self.target_status.value} + + def __str__(self): + return f'Id: {self.id}, Target Status: {self.target_status.value}' + + +class BulkModifyErrorResponse: + """The BulkModifyErrorResponse object defines the error response from the server + """ + def __init__(self, id: str, error: BasicErrorResponse, **kwargs): + """Creates an error object for a bulk modification call + + :param id: ID of the relevant resource + :type id: str + :param error: Error object response from the server + :type error: BasicErrorResponse + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + self.id = id + self.error = error + + @staticmethod + def from_dict(error_dict: Dict[str, Union[str, Dict]]): + """ + :param error_dict: Dict which includes the necessary values to create the object + :type error_dict: Dict[str, Union[str, Dict]] + + Returns: + Returns a class:`ai_api_client_sdk.models.base_models.BulkModifyErrorResponse` object + """ + error_dict['error'] = BasicErrorResponse.from_dict(error_dict['error']) + return BulkModifyErrorResponse(**error_dict) + + def __str__(self): + return f'Id: {self.id}, Error: {self.error.__str__()}' + + +class Order(Enum): + ASC = 'asc' + DESC = 'desc' + + +class Operation(AEnum): + CREATE = 'CREATE' + UPDATE = 'UPDATE' + DELETE = 'DELETE' + CASCADE_UPDATE = 'CASCADE-UPDATE' diff --git a/packages/base/ai_api_client_sdk/models/capabilities.py b/packages/base/ai_api_client_sdk/models/capabilities.py new file mode 100644 index 0000000..c94b036 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/capabilities.py @@ -0,0 +1,46 @@ +from typing import Any, Dict + +from ai_api_client_sdk.models.ai_api_meta import AIAPIMeta +from ai_api_client_sdk.models.extensions import Extensions + + +class Capabilities: + """The Capabilities object represents the metadata and capabilities of, and extensions to the AI API + + :param ai_api: Metadata and capabilities of the AI API + :type ai_api: class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMeta` + :param runtime_identifier: The name of runtime, defaults to None + :type runtime_identifier: str, optional + :param runtime_api_version: The version of the runtime, defaults to None + :type runtime_api_version: str, optional + :param description: description, defaults to None + :type description: str, optional + :param extensions: Extensions to the AI API, defaults to None + :type extensions: class:`ai_api_client_sdk.models.extensions.Extensions`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, ai_api: AIAPIMeta, runtime_identifier: str = None, runtime_api_version: str = None, + description: str = None, extensions: Extensions = None, **kwargs): + self.ai_api: AIAPIMeta = ai_api + self.runtime_identifier: str = runtime_identifier + self.runtime_api_version: str = runtime_api_version + self.description: str = description + self.extensions: Extensions = extensions + + @staticmethod + def from_dict(capabilities_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.capabilities.Capabilities` object, created + from the values in the dict provided as parameter + + :param capabilities_dict: Dict which includes the necessary values to create the object + :type capabilities_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.capabilities.Capabilities` + """ + capabilities_dict['ai_api'] = AIAPIMeta.from_dict(capabilities_dict['ai_api']) + if 'extensions' in capabilities_dict: + capabilities_dict['extensions'] = Extensions.from_dict(capabilities_dict['extensions']) + return Capabilities(**capabilities_dict) + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/configuration.py b/packages/base/ai_api_client_sdk/models/configuration.py new file mode 100644 index 0000000..2bc2a9d --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/configuration.py @@ -0,0 +1,67 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.models.scenario import Scenario + + +class Configuration: + """The Configuration object defines a configuration + + :param id: ID of the configuration + :type id: str + :param name: Name of the configuration + :type name: str + :param scenario_id: ID of the scenario which the configuration belongs to + :type scenario_id: str + :param executable_id: ID of the executable, which is configured + :type executable_id: str + :param created_at: Time when the configuration was created + :type created_at: datetime + :param parameter_bindings: List of the input parameters defined as key-value pairs, defaults to None + :type parameter_bindings: List[class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding`], optional + :param input_artifact_bindings: List of the input artifacts which are to be used by the executable, defaults to None + :type input_artifact_bindings: List[class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding`], + optional + :param scenario: A dict, which gives detailed information on scenario, defaults to None + :type scenario: class:`ai_api_client_sdk.models.scenario.Scenario`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, id: str, name: str, scenario_id: str, executable_id: str, created_at: datetime, + parameter_bindings: List[ParameterBinding] = None, + input_artifact_bindings: List[InputArtifactBinding] = None, scenario: Scenario = None, **kwargs): + self.id: str = id + self.name: str = name + self.scenario_id: str = scenario_id + self.executable_id: str = executable_id + self.parameter_bindings: List[ParameterBinding] = parameter_bindings + self.input_artifact_bindings: List[InputArtifactBinding] = input_artifact_bindings + self.created_at: datetime = created_at + self.scenario: Scenario = scenario + + def __str__(self): + return "Configuration id: " + str(self.id) + ", Configuration name: " + str(self.name) + + @staticmethod + def from_dict(configuration_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.configuration.Configuration` object, created from the values in the + dict provided as parameter + + :param configuration_dict: Dict which includes the necessary values to create the object + :type configuration_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.configuration.Configuration` + """ + configuration_dict['created_at'] = parse_datetime(configuration_dict['created_at']) + if configuration_dict.get('parameter_bindings'): + configuration_dict['parameter_bindings'] = \ + [ParameterBinding.from_dict(pb) for pb in configuration_dict['parameter_bindings']] + if configuration_dict.get('input_artifact_bindings'): + configuration_dict['input_artifact_bindings'] = \ + [InputArtifactBinding.from_dict(iab) for iab in configuration_dict['input_artifact_bindings']] + if configuration_dict.get('scenario'): + configuration_dict['scenario'] = Scenario.from_dict(configuration_dict['scenario']) + return Configuration(**configuration_dict) diff --git a/packages/base/ai_api_client_sdk/models/configuration_create_response.py b/packages/base/ai_api_client_sdk/models/configuration_create_response.py new file mode 100644 index 0000000..fd4dd9f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/configuration_create_response.py @@ -0,0 +1,20 @@ +from typing import Any, Dict + +from .base_models import BasicResponse + + +class ConfigurationCreateResponse(BasicResponse): + """The ConfigurationCreateResponse object defines the response of the configuration create request. Refer to + :class:`ai_api_client_sdk.models.base_models.BasicResponse`, for the object definition + """ + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse` object, + created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse` + """ + return ConfigurationCreateResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/configuration_query_response.py b/packages/base/ai_api_client_sdk/models/configuration_query_response.py new file mode 100644 index 0000000..76efab3 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/configuration_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .configuration import Configuration + + +class ConfigurationQueryResponse(QueryResponse): + """The ConfigurationQueryResponse object defines the response of the configuration query request + :param resources: List of the configurations returned from the server + :type resources: List[class:`ai_api_client_sdk.models.configuration.Configuration`] + :param count: Total number of the queried configurations + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Configuration], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse` object, + created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse` + """ + response_dict['resources'] = [Configuration.from_dict(r) for r in response_dict['resources']] + return ConfigurationQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/dataset_capabilities.py b/packages/base/ai_api_client_sdk/models/dataset_capabilities.py new file mode 100644 index 0000000..127d8b7 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/dataset_capabilities.py @@ -0,0 +1,41 @@ +from typing import Dict + + +class DatasetCapabilities: + """The DatasetCapabilities object represents the capabilities of the Dataset API + + :param upload: indicates whether uploading files is supported, defaults to True + :type upload: bool, optional + :param download: indicates whether downloading files is supported, defaults to True + :type download: bool, optional + :param delete: indicates whether deleting files is supported, defaults to True + :type delete: bool, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, upload: bool = True, download: bool = True, delete: bool = True, **kwargs): + self.upload: bool = upload + self.download: bool = download + self.delete: bool = delete + + @staticmethod + def from_dict(dataset_capabilities_dict: Dict[str, bool]): + """Returns a :class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities` object, created from + the values in the dict provided as parameter + + :param dataset_capabilities_dict: Dict which includes the necessary values to create the object + :type dataset_capabilities_dict: Dict[str, bool] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities` + """ + return DatasetCapabilities(**dataset_capabilities_dict) + + def __eq__(self, other): + if not isinstance(other, DatasetCapabilities): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/dataset_limits.py b/packages/base/ai_api_client_sdk/models/dataset_limits.py new file mode 100644 index 0000000..97070f8 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/dataset_limits.py @@ -0,0 +1,38 @@ +from typing import Dict + + +class DatasetLimits: + """The DatasetLimits object represents the limits of the Dataset API + + :param max_upload_file_size: Max size (in bytes) of a single uploaded file, defaults to 104857600 + :type max_upload_file_size: int, optional + :param max_files_per_dataset: Max number of files per dataset. <0 means unlimited, defaults to -1 + :type max_files_per_dataset: int, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, max_upload_file_size: int = 104857600, max_files_per_dataset: int = -1, **kwargs): + self.max_upload_file_size: int = max_upload_file_size + self.max_files_per_dataset: int = max_files_per_dataset + + @staticmethod + def from_dict(dataset_limits_dict: Dict[str, int]): + """Returns a :class:`ai_api_client_sdk.models.dataset_limits.DatasetLimits` object, created from the values in + the dict provided as parameter + + :param dataset_limits_dict: Dict which includes the necessary values to create the object + :type dataset_limits_dict: Dict[str, int] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.dataset_limits.DatasetLimits` + """ + return DatasetLimits(**dataset_limits_dict) + + def __eq__(self, other): + if not isinstance(other, DatasetLimits): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/deployment.py b/packages/base/ai_api_client_sdk/models/deployment.py new file mode 100644 index 0000000..62ac631 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/deployment.py @@ -0,0 +1,109 @@ +from datetime import datetime +from typing import Any, Dict + +import humps +from aenum import extend_enum + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.base_models import Operation +from ai_api_client_sdk.models.enactment import Enactment +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus + + +class Deployment(Enactment): + """The Deployment object defines a deployment + :param id: ID of the deployment + :type id: str + :param deployment_url: URL of the running deployment + :type deployment_url: str + :param configuration_id: ID of the configuration which configured the deployment + :type configuration_id: str + :param configuration_name: Name of the configuration which configured the deployment + :type configuration_name: str + :param scenario_id: ID of the scenario which the deployment belongs to + :type scenario_id: str + :param status: Status of the deployment + :type status: class:`ai_api_client_sdk.models.status.Status` + :param target_status: Target status of the deployment + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus` + :param created_at: Time when the deployment was created + :type created_at: datetime + :param modified_at: Time when the deployment was last modified + :type modified_at: datetime + :param status_message: A string, which gives information about the status of the deployment, defaults to None + :type status_message: str, optional + :param status_details: A dict, which gives detailed information about the status of the deployment, defaults to None + :type status_details: Dict[str, Any], optional + :param details: A dict, which gives information about the scaling and resources details of the deployment, defaults + to None + :type details: Dict[str, Any], optional + :param submission_time: Time when the deployment was submitted + :type submission_time: datetime, optional + :param start_time: Time when the deployment status changed to RUNNING + :type start_time: datetime, optional + :param completion_time: Time when the deployment status changed to DEAD/STOPPED + :type completion_time: datetime, optional + :param last_operation: Last operation applied to the deployment + :type last_operation: Operation, optional + :param latest_running_configuration_id: The configuration ID that was running, before a PATCH operation has modified + the configuration ID of the deployment. + :type latest_running_configuration_id: str, optional + :param ttl: Time to live for a deployment + :type ttl: str, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, id: str, deployment_url: str, configuration_id: str, configuration_name: str, scenario_id: str, + status: Status, target_status: TargetStatus, created_at: datetime, modified_at: datetime, + status_message: str = None, status_details: Dict[str, Any] = None, details: Dict[str, Any] = None, + submission_time: datetime = None, start_time: datetime = None, completion_time: datetime = None, + last_operation: Operation = None, latest_running_configuration_id: str = None, ttl: str = None, + **kwargs): + super().__init__(id=id, configuration_id=configuration_id, configuration_name=configuration_name, + scenario_id=scenario_id, status=status, target_status=target_status, created_at=created_at, + modified_at=modified_at, status_message=status_message, status_details=status_details, + submission_time=submission_time, start_time=start_time, completion_time=completion_time, + **kwargs) + self.deployment_url: str = deployment_url + self.last_operation: Operation = last_operation + self.latest_running_configuration_id: str = latest_running_configuration_id + self.details: Dict[str, Any] = details + self.ttl: str = ttl + + def __str__(self): + return "Deployment id: " + str(self.id) + + @staticmethod + def from_dict(deployment_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.deployment.Deployment` object, created from the values in the dict + provided as parameter + + :param deployment_dict: Dict which includes the necessary values to create the object + :type deployment_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.deployment.Deployment` + """ + deployment_dict['status'] = Status(deployment_dict['status']) + deployment_dict['target_status'] = TargetStatus(deployment_dict['target_status']) + deployment_dict['created_at'] = parse_datetime(deployment_dict['created_at']) + deployment_dict['modified_at'] = parse_datetime(deployment_dict['modified_at']) + if deployment_dict.get('submission_time'): + deployment_dict['submission_time'] = parse_datetime(deployment_dict['submission_time']) + if deployment_dict.get('start_time'): + deployment_dict['start_time'] = parse_datetime(deployment_dict['start_time']) + if deployment_dict.get('completion_time'): + deployment_dict['completion_time'] = parse_datetime(deployment_dict['completion_time']) + if deployment_dict.get('last_operation'): + last_operation_str = deployment_dict.get('last_operation') + try: + last_operation = Operation(last_operation_str) + except ValueError as ve: + if 'not a valid Operation' in ve.args[0]: + last_operation_name = humps.decamelize(last_operation_str).replace('-', '_') + extend_enum(Operation, last_operation_name, last_operation_str) + last_operation = Operation(last_operation_str) + else: + raise ve + deployment_dict['last_operation'] = last_operation + return Deployment(**deployment_dict) diff --git a/packages/base/ai_api_client_sdk/models/deployment_bulk_modify_response.py b/packages/base/ai_api_client_sdk/models/deployment_bulk_modify_response.py new file mode 100644 index 0000000..ef2466a --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/deployment_bulk_modify_response.py @@ -0,0 +1,35 @@ +from typing import List, Union, Dict + +from .base_models import BasicResponse, BulkModifyErrorResponse + + +class DeploymentBulkModifyResponse: + """The DeploymentBulkModifyResponse object defines the response to the deployments bulk modify request + :param deployments: Response to the bulk modify request of deployments + :type deployments: List[Union[BasicResponse, BulkModifyErrorResponse]] + """ + def __init__(self, deployments: List[Union[BasicResponse, BulkModifyErrorResponse]], **kwargs): + self.deployments: List[Union[BasicResponse, BulkModifyErrorResponse]] = deployments + + def __str__(self): + dbmr_str = '' + for d in self.deployments: + dbmr_str += f'{d.__str__()}\n' + return dbmr_str + + @staticmethod + def from_dict(response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]]): + """Returns a :class:`ai_api_client_sdk.models.Deployment_Bulk_Modify_Response.DeploymentBulkModifyResponse` + object, created from the values provided as parameter + + :param response_dict: Which includes the necessary values to create the object + :type response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.Deployment_Bulk_Modify_Response.DeploymentBulkModifyResponse` + """ + response_dict['deployments'] = [ + BulkModifyErrorResponse.from_dict(d) if 'error' in d else BasicResponse.from_dict(d) + for d in response_dict['deployments'] + ] + + return DeploymentBulkModifyResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/deployment_create_response.py b/packages/base/ai_api_client_sdk/models/deployment_create_response.py new file mode 100644 index 0000000..1b47b0e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/deployment_create_response.py @@ -0,0 +1,38 @@ +from typing import Any, Dict + +from .base_models import BasicResponse +from .status import Status + + +class DeploymentCreateResponse(BasicResponse): + """The DeploymentCreateResponse object defines the response of the deployment create query + :param id: ID of the deployment + :type id: str + :param message: Response message from the server + :type message: str + :param deployment_url: URL of the running deployment + :type deployment_url: str + :param status: Status of the deployment + :type status: class:`ai_api_client_sdk.models.status.Status` + :param ttl: Time to live for deployment + :type ttl: str, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, message: str, deployment_url: str, status: Status, ttl: str = None, **kwargs): + super().__init__(id=id, message=message, **kwargs) + self.deployment_url: str = deployment_url + self.status: Status = status + self.ttl: str = ttl + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse` object, + created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse` + """ + response_dict['status'] = Status(response_dict['status']) + return DeploymentCreateResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/deployment_get_status_response.py b/packages/base/ai_api_client_sdk/models/deployment_get_status_response.py new file mode 100644 index 0000000..45ad301 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/deployment_get_status_response.py @@ -0,0 +1,47 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.enactment_get_status_response import EnactmentGetStatusResponse +from ai_api_client_sdk.models.status import Status + + +class DeploymentGetStatusResponse(EnactmentGetStatusResponse): + """The DeploymentGetStatusResponse object defines the response of the deployment get status + :param id: ID of the deployment + :type id: str + :param configuration_id: ID of the configuration which configured the deployment + :type configuration_id: str + :param status: Status of the deployment + :type status: class:`ai_api_client_sdk.models.status.Status` + :param created_at: Time when the deployment was created + :type created_at: datetime + :param modified_at: Time when the deployment was last modified + :type modified_at: datetime + :param status_details: A dict, which gives detailed information about the status of the deployment, defaults to None + :type status_details: Dict[str, Any], optional + """ + + def __init__(self, id: str, configuration_id: str, + status: Status, created_at: datetime, modified_at: datetime, status_details: Dict[str, Any] = None, + details: Dict[str, Any] = None): + super().__init__(id=id, configuration_id=configuration_id, status=status, created_at=created_at, + modified_at=modified_at, status_details=status_details) + self.details: Dict[str, Any] = details + + def __str__(self): + return "Deployment id: " + str(self.id) + + @staticmethod + def from_dict(deployment_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse` + object, created from the values in the dict provided as parameter + :param deployment_dict: Dict which includes the necessary values to create the object + :type deployment_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse` + """ + deployment_dict['status'] = Status(deployment_dict['status']) + deployment_dict['created_at'] = parse_datetime(deployment_dict['created_at']) + deployment_dict['modified_at'] = parse_datetime(deployment_dict['modified_at']) + return DeploymentGetStatusResponse(**deployment_dict) diff --git a/packages/base/ai_api_client_sdk/models/deployment_query_response.py b/packages/base/ai_api_client_sdk/models/deployment_query_response.py new file mode 100644 index 0000000..0c448d8 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/deployment_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .deployment import Deployment + + +class DeploymentQueryResponse(QueryResponse): + """The DeploymentQueryResponse object defines the response of the deployment query request + :param resources: List of the deployments returned from the server + :type resources: List[class:`ai_api_client_sdk.models.deployment.Deployment`] + :param count: Total number of the queried deployments + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Deployment], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse` + """ + response_dict['resources'] = [Deployment.from_dict(r) for r in response_dict['resources']] + return DeploymentQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/enactment.py b/packages/base/ai_api_client_sdk/models/enactment.py new file mode 100644 index 0000000..b1ee8bb --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/enactment.py @@ -0,0 +1,58 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus + + +class Enactment: + """Enactment object is base class for Execution/Deployment, defining their common attributes + + :param id: ID of the enactment + :type id: str + :param configuration_id: ID of the configuration which configured the enactment + :type configuration_id: str + :param configuration_name: Name of the configuration which configured the enactment + :type configuration_name: str + :param scenario_id: ID of the scenario which the enactment belongs to + :type scenario_id: str + :param status: Status of the enactment + :type status: class:`ai_api_client_sdk.models.status.Status` + :param target_status: Target status of the enactment + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus` + :param created_at: Time when the enactment was created + :type created_at: datetime + :param modified_at: Time when the enactment was last modified + :type modified_at: datetime + :param status_message: A string, which gives information about the status of the enactment, defaults to None + :type status_message: str, optional + :param status_details: A dict, which gives detailed information about the status of the enactment, defaults to None + :type status_details: Dict[str, Any], optional + :param submission_time: Time when the enactment was submitted + :type submission_time: datetime, optional + :param start_time: Time when the enactment status changed to RUNNING + :type start_time: datetime, optional + :param completion_time: Time when the enactment status changed to COMPLETED/DEAD/STOPPED + :type completion_time: datetime, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, configuration_id: str, configuration_name, scenario_id: str, status: Status, + target_status: TargetStatus, created_at: datetime, modified_at: datetime, status_message: str = None, + status_details: Dict[str, Any] = None, submission_time: datetime = None, start_time: datetime = None, + completion_time: datetime = None, **kwargs): + self.id: str = id + self.configuration_id: str = configuration_id + self.configuration_name: str = configuration_name + self.scenario_id: str = scenario_id + self.status: Status = status + self.target_status: TargetStatus = target_status + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + self.status_message: str = status_message + self.status_details: Dict[str, Any] = status_details + self.submission_time: datetime = submission_time + self.start_time: datetime = start_time + self.completion_time: datetime = completion_time + + def __str__(self): + return "Enactment id: " + str(self.id) diff --git a/packages/base/ai_api_client_sdk/models/enactment_get_status_response.py b/packages/base/ai_api_client_sdk/models/enactment_get_status_response.py new file mode 100644 index 0000000..70da5a4 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/enactment_get_status_response.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.models.status import Status + + +class EnactmentGetStatusResponse: + """EnactmentGetStatusResponse object defines the response from the server to get status of Execution/Deployment + :param id: ID of the enactment + :type id: str + :param configuration_id: ID of the configuration which configured the enactment + :type configuration_id: str + :param status: Status of the enactment + :type status: class:`ai_api_client_sdk.models.status.Status` + :param created_at: Time when the enactment was created + :type created_at: datetime + :param modified_at: Time when the enactment was last modified + :type modified_at: datetime + :param status_details: A dict, which gives detailed information about the status of the enactment, defaults to None + :type status_details: Dict[str, Any], optional + """ + def __init__(self, id: str, configuration_id: str, status: Status, + created_at: datetime, modified_at: datetime, status_details: Dict[str, Any] = None): + self.id: str = id + self.configuration_id: str = configuration_id + self.status: Status = status + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + self.status_details: Dict[str, Any] = status_details + + def __str__(self): + return "Enactment id: " + str(self.id) diff --git a/packages/base/ai_api_client_sdk/models/executable.py b/packages/base/ai_api_client_sdk/models/executable.py new file mode 100644 index 0000000..1393b4f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/executable.py @@ -0,0 +1,90 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.input_artifact import InputArtifact +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.output_artifact import OutputArtifact +from ai_api_client_sdk.models.parameter import Parameter + + +class Executable: + """The Executable object defines an executable + :param id: ID of the Executable + :type id: str + :param scenario_id: ID of the scenario which the executable belongs to + :type scenario_id: str + :param version_id: ID of the version of the scenario, the executable belongs to + :type version_id: str + :param name: Name of the executable + :type name: str + :param deployable: Flag which defines if the executable is deployable + :type deployable: bool + :param created_at: Time when the executable was created + :type created_at: datetime + :param modified_at: Time when the executable was last modified + :type modified_at: datetime + :param description: Description of the executable, defaults to None + :type description: str, optional + :param parameters: List of the parameters of the executable, defaults to None + :type parameters: List[class:`ai_api_client_sdk.models.parameter.Parameter`], optional + :param input_artifacts: List of the input artifacts which are to be used by the executable, defaults to None + :type input_artifacts: List[class:`ai_api_client_sdk.models.input_artifact.InputArtifact`], optional + :param output_artifacts: List of the artifacts to be created by the executable, defaults to None + :type output_artifacts: List[class:`ai_api_client_sdk.models.output_artifact.OutputArtifact`], optional + :param labels: List of the labels of the executable, defaults to None + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, id: str, scenario_id: str, version_id: str, name: str, deployable: bool, created_at: datetime, + modified_at: datetime, description: str = None, parameters: List[Parameter] = None, + input_artifacts: List[InputArtifact] = None, output_artifacts: List[OutputArtifact] = None, + labels: List[Label] = None, **kwargs): + self.id: str = id + self.scenario_id: str = scenario_id + self.version_id: str = version_id + self.name: str = name + self.description: str = description + self.deployable: bool = deployable + self.parameters: List[Parameter] = parameters + self.input_artifacts: List[InputArtifact] = input_artifacts + self.output_artifacts: List[OutputArtifact] = output_artifacts + self.labels: List[Label] = labels + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + + def __eq__(self, other): + if not isinstance(other, Executable): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return "Executable id: " + str(self.id) + ", Executable description: " + str(self.description) + + @staticmethod + def from_dict(executable_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.executable.Executable` object, created from the values in the dict + provided as parameter + + :param executable_dict: Dict which includes the necessary values to create the object + :type executable_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.executable.Executable` + """ + executable_dict['created_at'] = parse_datetime(executable_dict['created_at']) + executable_dict['modified_at'] = parse_datetime(executable_dict['modified_at']) + if executable_dict.get('parameters'): + executable_dict['parameters'] = [Parameter.from_dict(p) for p in executable_dict['parameters']] + if executable_dict.get('input_artifacts'): + executable_dict['input_artifacts'] = \ + [InputArtifact.from_dict(ia) for ia in executable_dict['input_artifacts']] + if executable_dict.get('output_artifacts'): + executable_dict['output_artifacts'] = \ + [OutputArtifact.from_dict(oa) for oa in executable_dict['output_artifacts']] + if executable_dict.get('labels'): + executable_dict['labels'] = [Label.from_dict(l) for l in executable_dict['labels']] + return Executable(**executable_dict) diff --git a/packages/base/ai_api_client_sdk/models/executable_query_response.py b/packages/base/ai_api_client_sdk/models/executable_query_response.py new file mode 100644 index 0000000..89f18e7 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/executable_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .executable import Executable + + +class ExecutableQueryResponse(QueryResponse): + """The ExecutableQueryResponse object defines the response of the executable query request + :param resources: List of the executables returned from the server + :type resources: List[class:`ai_api_client_sdk.models.executable.Executable`] + :param count: Total number of the queried executables + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Executable], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse` + """ + response_dict['resources'] = [Executable.from_dict(r) for r in response_dict['resources']] + return ExecutableQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution.py b/packages/base/ai_api_client_sdk/models/execution.py new file mode 100644 index 0000000..04e5511 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution.py @@ -0,0 +1,84 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.enactment import Enactment +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus + + +class Execution(Enactment): + """The Execution object defines an execution + + :param id: ID of the execution + :type id: str + :param configuration_id: ID of the configuration which configured the execution + :type configuration_id: str + :param configuration_name: Name of the configuration which configured the execution + :type configuration_name: str + :param scenario_id: ID of the scenario which the execution belongs to + :type scenario_id: str + :param status: Status of the execution + :type status: class:`ai_api_client_sdk.models.status.Status` + :param target_status: Target status of the execution + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus` + :param execution_schedule_id: ID of the execution schedule, defaults to None + :type execution_schedule_id: str, optional + :param created_at: Time when the execution was created + :type created_at: datetime + :param modified_at: Time when the execution was last modified + :type modified_at: datetime + :param output_artifacts: List of the artifacts created by the execution, defaults to None + :type output_artifacts: List[class:`ai_api_client_sdk.models.artifact.Artifact`], optional + :param status_message: Gives information about the status of the execution, defaults to None + :type status_message: str, optional + :param status_details: A dict, which gives detailed information about the status of the execution, defaults to None + :type status_details: Dict[str, Any], optional + :param submission_time: Time when the execution was submitted + :type submission_time: datetime, optional + :param start_time: Time when the execution status changed to RUNNING + :type start_time: datetime, optional + :param completion_time: Time when the execution status changed to COMPLETED/DEAD/STOPPED + :type completion_time: datetime, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, configuration_id: str, configuration_name: str, scenario_id: str, status: Status, + target_status: TargetStatus, created_at: datetime, modified_at: datetime, + output_artifacts: List[Artifact] = None, status_message: str = None, + status_details: Dict[str, Any] = None, submission_time: datetime = None, start_time: datetime = None, + execution_schedule_id: str = None, completion_time: datetime = None, **kwargs): + super().__init__(id=id, configuration_id=configuration_id, configuration_name=configuration_name, + scenario_id=scenario_id, status=status, target_status=target_status, created_at=created_at, + modified_at=modified_at, status_message=status_message, status_details=status_details, + submission_time=submission_time, start_time=start_time, completion_time=completion_time, + **kwargs) + self.output_artifacts: List[Artifact] = output_artifacts + self.execution_schedule_id = execution_schedule_id + + def __str__(self): + return "Execution id: " + str(self.id) + + @staticmethod + def from_dict(execution_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution.Execution` object, created from the values in the dict + provided as parameter + + :param execution_dict: Dict which includes the necessary values to create the object + :type execution_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution.Execution` + """ + execution_dict['status'] = Status(execution_dict['status']) + execution_dict['target_status'] = TargetStatus(execution_dict['target_status']) + execution_dict['created_at'] = parse_datetime(execution_dict['created_at']) + execution_dict['modified_at'] = parse_datetime(execution_dict['modified_at']) + if execution_dict.get('submission_time'): + execution_dict['submission_time'] = parse_datetime(execution_dict['submission_time']) + if execution_dict.get('start_time'): + execution_dict['start_time'] = parse_datetime(execution_dict['start_time']) + if execution_dict.get('completion_time'): + execution_dict['completion_time'] = parse_datetime(execution_dict['completion_time']) + if execution_dict.get('output_artifacts'): + execution_dict['output_artifacts'] = [Artifact.from_dict(oa) for oa in execution_dict['output_artifacts']] + return Execution(**execution_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_bulk_modify_response.py b/packages/base/ai_api_client_sdk/models/execution_bulk_modify_response.py new file mode 100644 index 0000000..a4bc8bd --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_bulk_modify_response.py @@ -0,0 +1,35 @@ +from typing import List, Union, Dict + +from .base_models import BasicResponse, BulkModifyErrorResponse + + +class ExecutionBulkModifyResponse: + """The ExecutionBulkModifyResponse object defines the response to the executions bulk modify request + :param exeuctions: Response to the bulk modify request of executions + :type exeuctions: List[Union[BasicResponse, BulkModifyErrorResponse]] + """ + def __init__(self, executions: List[Union[BasicResponse, BulkModifyErrorResponse]], **kwargs): + self.executions: List[Union[BasicResponse, BulkModifyErrorResponse]] = executions + + def __str__(self): + ebmr_str = '' + for d in self.executions: + ebmr_str += f'{d.__str__()}\n' + return ebmr_str + + @staticmethod + def from_dict(response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]]): + """Returns a :class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse` object, + created from the values provided as parameter + + :param response_dict: Which includes the necessary values to create the object + :type response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse` + """ + response_dict['executions'] = [ + BulkModifyErrorResponse.from_dict(d) if 'error' in d else BasicResponse.from_dict(d) + for d in response_dict['executions'] + ] + + return ExecutionBulkModifyResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_create_response.py b/packages/base/ai_api_client_sdk/models/execution_create_response.py new file mode 100644 index 0000000..5a6f6d1 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_create_response.py @@ -0,0 +1,32 @@ +from typing import Any, Dict + +from .base_models import BasicResponse +from .status import Status + + +class ExecutionCreateResponse(BasicResponse): + """The ExecutionCreateResponse object defines the response of the execution create request + :param id: ID of the execution + :type id: str + :param message: Response message from the server + :type message: str + :param status: Status of the execution + :type status: class:`ai_api_client_sdk.models.status.Status` + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, message: str, status: Status, **kwargs): + super().__init__(id=id, message=message, **kwargs) + self.status: Status = status + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse` object, + created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse` + """ + response_dict['status'] = Status(response_dict['status']) + return ExecutionCreateResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_get_status_response.py b/packages/base/ai_api_client_sdk/models/execution_get_status_response.py new file mode 100644 index 0000000..392ac08 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_get_status_response.py @@ -0,0 +1,44 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.enactment_get_status_response import EnactmentGetStatusResponse +from ai_api_client_sdk.models.status import Status + + +class ExecutionGetStatusResponse(EnactmentGetStatusResponse): + """The ExecutionGetStatusResponse object defines the response of the execution get status + :param id: ID of the execution + :type id: str + :param configuration_id: ID of the configuration which configured the execution + :type configuration_id: str + :param status: Status of the execution + :type status: class:`ai_api_client_sdk.models.status.Status` + :param created_at: Time when the execution was created + :type created_at: datetime + :param modified_at: Time when the execution was last modified + :type modified_at: datetime + :param status_details: A dict, which gives detailed information about the status of the execution, defaults to None + :type status_details: Dict[str, Any], optional + """ + def __init__(self, id: str, configuration_id: str, + status: Status, created_at: datetime, modified_at: datetime, status_details: Dict[str, Any] = None): + super().__init__(id=id, configuration_id=configuration_id, status=status, created_at=created_at, + modified_at=modified_at, status_details=status_details) + + def __str__(self): + return "Execution id: " + str(self.id) + + @staticmethod + def from_dict(execution_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse` object, + created from the values in the dict provided as parameter + :param execution_dict: Dict which includes the necessary values to create the object + :type execution_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse` + """ + execution_dict['status'] = Status(execution_dict['status']) + execution_dict['created_at'] = parse_datetime(execution_dict['created_at']) + execution_dict['modified_at'] = parse_datetime(execution_dict['modified_at']) + return ExecutionGetStatusResponse(**execution_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_query_response.py b/packages/base/ai_api_client_sdk/models/execution_query_response.py new file mode 100644 index 0000000..e1a422e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .execution import Execution + + +class ExecutionQueryResponse(QueryResponse): + """The ExecutionQueryResponse object defines the response of the execution query request + :param resources: List of the executions returned from the server + :type resources: List[class:`ai_api_client_sdk.models.execution.Execution`] + :param count: Total number of the queried executions + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Execution], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse` + """ + response_dict['resources'] = [Execution.from_dict(r) for r in response_dict['resources']] + return ExecutionQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_schedule.py b/packages/base/ai_api_client_sdk/models/execution_schedule.py new file mode 100644 index 0000000..c4328a6 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_schedule.py @@ -0,0 +1,63 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.status import ScheduleStatus + + +class ExecutionSchedule: + """An Execution Schedule allows to trigger executions periodically + + :param id: ID of the execution schedule + :type id: str + :param name: Name of the execution schedule + :type name: str + :param cron: Cron defining the schedule to run the executions + :type cron: str + :param configuration_id: ID of the configuration for the execution schedule + :type configuration_id: str + :param status: status of the execution schedule + :type status: str + :param created_at: Time when the execution schedule was created + :type created_at: datetime, optional + :param modified_at: Time when the execution schedule was last modified + :type modified_at: datetime, optional + :param start: Start time of the execution schedule + :type start: datetime, optional + :param end: End time of the execution schedule + :type end: datetime, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, name: str, cron: str, configuration_id: str, status: ScheduleStatus, + created_at: datetime, modified_at: datetime, start: datetime = None, end: datetime = None, **kwargs): + self.id: str = id + self.name = name + self.cron: str = cron + self.configuration_id: str = configuration_id + self.status: ScheduleStatus = status + self.start: datetime = start + self.end: datetime = end + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + + def __str__(self): + return "Execution Schedule id: " + str(self.id) + + @staticmethod + def from_dict(execution_schedule_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_schedule.Schedule` object, created from the values in + the dict provided as parameter + + :param execution_schedule_dict: Dict which includes the necessary values to create the object + :type execution_schedule_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_schedule.Schedule` + """ + execution_schedule_dict['status'] = ScheduleStatus(execution_schedule_dict['status']) + execution_schedule_dict['created_at'] = parse_datetime(execution_schedule_dict['created_at']) + execution_schedule_dict['modified_at'] = parse_datetime(execution_schedule_dict['modified_at']) + if execution_schedule_dict.get('start'): + execution_schedule_dict['start'] = parse_datetime(execution_schedule_dict['start']) + if execution_schedule_dict.get('end'): + execution_schedule_dict['end'] = parse_datetime(execution_schedule_dict['end']) + return ExecutionSchedule(**execution_schedule_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_schedule_create_response.py b/packages/base/ai_api_client_sdk/models/execution_schedule_create_response.py new file mode 100644 index 0000000..437e080 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_schedule_create_response.py @@ -0,0 +1,20 @@ +from typing import Any, Dict + +from .base_models import BasicResponse + + +class ExecutionScheduleCreateResponse(BasicResponse): + """The ExecutionScheduleCreateResponse object defines the response of the execution schedule create request. + Refer to :class:`ai_api_client_sdk.models.base_models.BasicResponse`, for the object definition + """ + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse` + """ + return ExecutionScheduleCreateResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/execution_schedule_query_response.py b/packages/base/ai_api_client_sdk/models/execution_schedule_query_response.py new file mode 100644 index 0000000..45b7569 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/execution_schedule_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .execution_schedule import ExecutionSchedule + + +class ExecutionScheduleQueryResponse(QueryResponse): + """The ExecutionScheduleQueryResponse object defines the response of the execution schedule query request + :param resources: List of the execution schedules returned from the server + :type resources: List[class:`ai_api_client_sdk.models.execution_schedule.ExecutionSchedule`] + :param count: Total number of the queried execution schedules + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[ExecutionSchedule], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.execution_schedule_query_response.ExecutionScheduleQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.execution_schedule_query_response.ExecutionScheduleQueryResponse` + """ + response_dict['resources'] = [ExecutionSchedule.from_dict(r) for r in response_dict['resources']] + return ExecutionScheduleQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/extensions.py b/packages/base/ai_api_client_sdk/models/extensions.py new file mode 100644 index 0000000..98c20f7 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/extensions.py @@ -0,0 +1,53 @@ +from typing import Any, Dict + +from ai_api_client_sdk.models.extensions_analytics import ExtensionsAnalytics +from ai_api_client_sdk.models.extensions_dataset import ExtensionsDataset +from ai_api_client_sdk.models.extensions_resource_groups import ExtensionsResourceGroups + + +class Extensions: + """The Extensions object represents the extensions to the AI API + + :param analytics: Metadata and capabilities of the Analytics API, defaults to None + :type analytics: class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics`, optional + :param resource_groups: Metadata and capabilities of the Resource Groups API, defaults to None + :type resource_groups: class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups`, + optional + :param dataset: Metadata and capabilities of the Dataset API, defaults to None + :type dataset: class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, analytics: ExtensionsAnalytics = None, resource_groups: ExtensionsResourceGroups = None, + dataset: ExtensionsDataset = None, **kwargs): + self.analytics: ExtensionsAnalytics = analytics + self.resource_groups: ExtensionsResourceGroups = resource_groups + self.dataset: ExtensionsDataset = dataset + + @staticmethod + def from_dict(extensions_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.extensions.Extensions` object, created from the values in the + dict provided as parameter + + :param extensions_dict: Dict which includes the necessary values to create the object + :type extensions_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.extensions.Extensions` + """ + if 'analytics' in extensions_dict: + extensions_dict['analytics'] = ExtensionsAnalytics.from_dict(extensions_dict['analytics']) + if 'resource_groups' in extensions_dict: + extensions_dict['resource_groups'] = ExtensionsResourceGroups.from_dict(extensions_dict['resource_groups']) + if 'dataset' in extensions_dict: + extensions_dict['dataset'] = ExtensionsDataset.from_dict(extensions_dict['dataset']) + return Extensions(**extensions_dict) + + def __eq__(self, other): + if not isinstance(other, Extensions): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/extensions_analytics.py b/packages/base/ai_api_client_sdk/models/extensions_analytics.py new file mode 100644 index 0000000..635733b --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/extensions_analytics.py @@ -0,0 +1,35 @@ +from typing import Dict + + +class ExtensionsAnalytics: + """The ExtensionsAnalytics object represents the metadata and capabilities of the Analytics API + + :param version: Version of the Analytics API + :type version: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, version: str, **kwargs): + self.version: str = version + + @staticmethod + def from_dict(extensions_analytics_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics` object, created from + the values in the dict provided as parameter + + :param extensions_analytics_dict: Dict which includes the necessary values to create the object + :type extensions_analytics_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics` + """ + return ExtensionsAnalytics(**extensions_analytics_dict) + + def __eq__(self, other): + if not isinstance(other, ExtensionsAnalytics): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/extensions_dataset.py b/packages/base/ai_api_client_sdk/models/extensions_dataset.py new file mode 100644 index 0000000..f690b29 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/extensions_dataset.py @@ -0,0 +1,49 @@ +from typing import Any, Dict + +from ai_api_client_sdk.models.dataset_capabilities import DatasetCapabilities +from ai_api_client_sdk.models.dataset_limits import DatasetLimits + + +class ExtensionsDataset: + """The ExtensionsDataset object represents the metadata and capabilities of the Dataset API + + :param version: Version of the Dataset API + :type version: str + :param capabilities: Capabilities of the Dataset API, defaults to None + :type capabilities: class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities`, optional + :param limits: Limits of the Dataset API, defaults to None + :type limits: class:`ai_api_client_sdk.models.dataset_limits.DatasetLimits`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, version: str, capabilities: DatasetCapabilities = None, limits: DatasetLimits = None, **kwargs): + self.version: str = version + self.capabilities: DatasetCapabilities = capabilities + self.limits: DatasetLimits = limits + + @staticmethod + def from_dict(extensions_dataset_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset` object, created from the + values in the dict provided as parameter + + :param extensions_dataset_dict: Dict which includes the necessary values to create the object + :type extensions_dataset_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset` + """ + if 'capabilities' in extensions_dataset_dict: + extensions_dataset_dict['capabilities'] = \ + DatasetCapabilities.from_dict(extensions_dataset_dict['capabilities']) + if 'limits' in extensions_dataset_dict: + extensions_dataset_dict['limits'] = DatasetLimits.from_dict(extensions_dataset_dict['limits']) + return ExtensionsDataset(**extensions_dataset_dict) + + def __eq__(self, other): + if not isinstance(other, ExtensionsDataset): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/extensions_resource_groups.py b/packages/base/ai_api_client_sdk/models/extensions_resource_groups.py new file mode 100644 index 0000000..c6509c1 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/extensions_resource_groups.py @@ -0,0 +1,35 @@ +from typing import Dict + + +class ExtensionsResourceGroups: + """The ExtensionsResourceGroups object represents the metadata and capabilities of the Resource Groups API + + :param version: Version of the Resource Groups API + :type version: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, version: str, **kwargs): + self.version: str = version + + @staticmethod + def from_dict(extensions_resource_groups_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups` object, + created from the values in the dict provided as parameter + + :param extensions_resource_groups_dict: Dict which includes the necessary values to create the object + :type extensions_resource_groups_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups` + """ + return ExtensionsResourceGroups(**extensions_resource_groups_dict) + + def __eq__(self, other): + if not isinstance(other, ExtensionsResourceGroups): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/healthz_status.py b/packages/base/ai_api_client_sdk/models/healthz_status.py new file mode 100644 index 0000000..57d3463 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/healthz_status.py @@ -0,0 +1,36 @@ +from enum import Enum +from typing import Any, Dict + + +class HealthStatus(Enum): + READY = 'READY' + NOT_READY = 'NOT READY' + + +class HealthzStatus: + """The HealthzStatus object defines the response of the healthz endpoint + :param status: Health status of the server + :type status: class:`ai_api_client_sdk.models.healthz_status.HealthStatus` + :param message: Response message from the server + :type message: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, status: HealthStatus, message: str, **kwargs): + self.status: HealthStatus = status + self.message: str = message + + def __str__(self): + return "Healthz status message: " + str(self.message) + + @staticmethod + def from_dict(healthz_status_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.healthz_status.HealthzStatus` object, created from the values in + the dict provided as parameter + + :param healthz_status_dict: Dict which includes the necessary values to create the object + :type healthz_status_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.healthz_status.HealthzStatus` + """ + healthz_status_dict['status'] = HealthStatus(healthz_status_dict['status']) + return HealthzStatus(**healthz_status_dict) diff --git a/packages/base/ai_api_client_sdk/models/input_artifact.py b/packages/base/ai_api_client_sdk/models/input_artifact.py new file mode 100644 index 0000000..cbefbdc --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/input_artifact.py @@ -0,0 +1,46 @@ +from typing import Any, Dict, List + +from ai_api_client_sdk.models.label import Label + + +class InputArtifact: + """The InputArtifact object defines the input artifact specified in the executable definition. + :param name: name of artifact + :type name: str + :param kind: kind of artifact (Dataset, Model, ResultSet) + :type kind: str + :param description: description of artifact + :type description: str + :param labels: labels for artifact + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + """ + + def __str__(self): + return "Input artifact name: " + str(self.name) + ", Input artifact kind: " + str(self.kind) + \ + ", Input artifact description: " + str(self.description) + + def __eq__(self, other): + if not isinstance(other, InputArtifact): + return False + return self.name == other.name and self.kind == other.kind and self.description == other.description and \ + self.labels == other.labels + + def __init__(self, name: str, kind: str = None, description: str = None, labels: List[Label] = None): + self.name: str = name + self.kind: str = kind + self.description: str = description + self.labels: List[Label] = labels + + @staticmethod + def from_dict(input_artifact_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.input_artifact.InputArtifact` object, created from the values + in the dict provided as parameter + + :param input_artifact_dict: Dict which includes the necessary values to create the object + :type input_artifact_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.input_artifact.InputArtifact` + """ + if input_artifact_dict.get('labels'): + input_artifact_dict['labels'] = [Label.from_dict(l) for l in input_artifact_dict['labels']] + return InputArtifact(**input_artifact_dict) diff --git a/packages/base/ai_api_client_sdk/models/input_artifact_binding.py b/packages/base/ai_api_client_sdk/models/input_artifact_binding.py new file mode 100644 index 0000000..86c1cd6 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/input_artifact_binding.py @@ -0,0 +1,40 @@ +from typing import Dict + + +class InputArtifactBinding: + """The InputArtifactBinding object defines the input artifact specified in the configuration. + + :param key: matches the input artifact name in the executable definition + :type key: str + :param id: ID of the artifact + :type id: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, key: str, artifact_id: str, **kwargs): + self.key: str = key + self.artifact_id: str = artifact_id + + def to_dict(self) -> Dict[str, str]: + """Returns the attributes of the object as a dictionary + + :return: A dict, including all the attributes of the object + :rtype: Dict[str, str] + """ + return {'key': self.key, 'artifact_id': self.artifact_id} + + def __eq__(self, other): + if not isinstance(other, InputArtifactBinding): + return False + return self.key == other.key and self.artifact_id == other.artifact_id + + @staticmethod + def from_dict(input_artifact_binding_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding` object, created from + the values in the dict provided as parameter + + :param input_artifact_binding_dict: Dict which includes the necessary values to create the object + :type input_artifact_binding_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding` + """ + return InputArtifactBinding(**input_artifact_binding_dict) diff --git a/packages/base/ai_api_client_sdk/models/label.py b/packages/base/ai_api_client_sdk/models/label.py new file mode 100644 index 0000000..165efa6 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/label.py @@ -0,0 +1,24 @@ +from typing import Dict + +from .base_models import KeyValue + + +class Label(KeyValue): + """The Label object defines a label as a key-value pair. Refer to + :class:`ai_api_client_sdk.models.base_models.KeyValue`, for the object definition + """ + + def __str__(self): + return "Label key: " + str(self.key) + ", Label value: " + str(self.value) + + @staticmethod + def from_dict(label_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.label.Label` object, created from the values in the dict provided + as parameter + + :param label_dict: Dict which includes the necessary values to create the object + :type label_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.label.Label` + """ + return Label(**label_dict) diff --git a/packages/base/ai_api_client_sdk/models/log_response.py b/packages/base/ai_api_client_sdk/models/log_response.py new file mode 100644 index 0000000..9182c3e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/log_response.py @@ -0,0 +1,82 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime + + +class LogResultItem: + """The LogResultItem object defines each item in the log response + :param msg: log message + :type msg: str + :param timestamp: timestamp corresponding to the log message + :type timestamp: datetime + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, msg: str, timestamp: datetime, **kwargs): + self.msg: str = msg + self.timestamp: datetime = timestamp + + @staticmethod + def from_dict(log_result_item_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.log_response.LogResultItem` object, created from the values + in the dict provided as parameter + + :param log_result_item_dict: Dict which includes the necessary values to create the object + :type log_result_item_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.log_response.LogResultItem` + """ + log_result_item_dict['timestamp'] = parse_datetime(log_result_item_dict['timestamp']) + return LogResultItem(**log_result_item_dict) + + +class LogResponseData: + """The LogResponseData object defines the data of the log response + :param result: result of the log query + :type result: List[class:`ai_api_client_sdk.models.log_response.LogResponseResultItem`] + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, result: List[LogResultItem], **kwargs): + self.result: List[LogResultItem] = result + + @staticmethod + def from_dict(log_response_data_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.log_response.LogResponseData` object, created from the values + in the dict provided as parameter + + :param log_response_data_dict: Dict which includes the necessary values to create the object + :type log_response_data_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.log_response.LogResponseData` + """ + log_response_data_dict['result'] = [LogResultItem.from_dict(r) for r in log_response_data_dict['result']] + return LogResponseData(**log_response_data_dict) + + +class LogResponse: + """The LogResponse object defines the response of the log request + :param data: log response data + :type data: class:`ai_api_client_sdk.models.log_response.LogResponseData` + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, data: LogResponseData, **kwargs): + self.data: LogResponseData = data + + def __str__(self): + res_str = "Log response messages: " + for result_item in self.data.result: + res_str += str(result_item.msg) + ", " + return res_str[:len(res_str) - 2] + + @staticmethod + def from_dict(log_response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.log_response.LogResponse` object, created from the values + in the dict provided as parameter + + :param log_response_dict: Dict which includes the necessary values to create the object + :type log_response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.log_response.LogResponse` + """ + log_response_dict['data'] = LogResponseData.from_dict(log_response_dict['data']) + return LogResponse(**log_response_dict) diff --git a/packages/base/ai_api_client_sdk/models/metric.py b/packages/base/ai_api_client_sdk/models/metric.py new file mode 100644 index 0000000..00afb7b --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metric.py @@ -0,0 +1,71 @@ +from copy import deepcopy +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime, DATETIME_FORMAT +from ai_api_client_sdk.models.metric_label import MetricLabel + + +class Metric: + """The Metric object, defines a single metric. + + :param name: Name of the metric + :type name: str + :param value: numeric value of the metric + :type value: float + :param timestamp: Time when the metric was created + :type timestamp: datetime + :param step: any measurement of training progress (number of training iterations, number of epochs, etc.) + :type step: int + :param labels: List of the labels of the metric, defaults to None + :type labels: List[class:`ai_api_client_sdk.models.metric_label.MetricLabel`] + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, name: str, value: float, timestamp: datetime, step: int = None, + labels: List[MetricLabel] = None, **kwargs): + self.name: str = name + self.value: float = value + self.timestamp: datetime = timestamp + self.step: int = step if step is not None else 0 + self.labels: List[MetricLabel] = labels if labels is not None else [] + + def __eq__(self, other): + if not isinstance(other, Metric): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return "Metric name: " + str(self.name) + ", Metric value: " + str(self.value) + + @staticmethod + def from_dict(metric_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.metric.Metric` object, created from the values in the dict + provided as parameter + + :param metric_dict: Dict which includes the necessary values to create the object + :type metric_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metric.Metric` + """ + if metric_dict.get('timestamp'): + metric_dict['timestamp'] = parse_datetime(metric_dict['timestamp']) + if metric_dict.get('labels'): + metric_dict['labels'] = [MetricLabel.from_dict(l) for l in metric_dict['labels']] + return Metric(**metric_dict) + + def to_dict(self): + """Returns the attributes of the object as a dictionary + + :return: A dict, including all the attributes of the object + :rtype: Dict[str, str] + """ + metric_dict = deepcopy(self.__dict__) + if metric_dict['labels']: + metric_dict['labels'] = [l.to_dict() for l in self.labels] + if metric_dict['timestamp']: + metric_dict['timestamp'] = metric_dict['timestamp'].strftime(DATETIME_FORMAT) + + return metric_dict diff --git a/packages/base/ai_api_client_sdk/models/metric_custom_info.py b/packages/base/ai_api_client_sdk/models/metric_custom_info.py new file mode 100644 index 0000000..e1e3faf --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metric_custom_info.py @@ -0,0 +1,21 @@ +from typing import Dict + +from .base_models import NameValue + + +class MetricCustomInfo(NameValue): + """The MetricCustomInfo object defines rendering/semantic information regarding certain metric for consuming + application or complex metrics in JSON format, as a name-value pair. Refer to + :class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition + """ + @staticmethod + def from_dict(metric_custom_info_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo` object, created from the + values in the dict provided as parameter + + :param metric_custom_info_dict: Dict which includes the necessary values to create the object + :type metric_custom_info_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo` + """ + return MetricCustomInfo(**metric_custom_info_dict) diff --git a/packages/base/ai_api_client_sdk/models/metric_label.py b/packages/base/ai_api_client_sdk/models/metric_label.py new file mode 100644 index 0000000..3c5489e --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metric_label.py @@ -0,0 +1,28 @@ +from typing import Dict + +from .base_models import NameValue + + +class MetricLabel(NameValue): + """The MetricLabel object defines a metric label as a name-value pair. Refer to + :class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition + """ + @staticmethod + def from_dict(metric_label_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.metric_label.MetricLabel` object, created from the values in the + dict provided as parameter + + :param metric_label_dict: Dict which includes the necessary values to create the object + :type metric_label_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metric_label.MetricLabel` + """ + return MetricLabel(**metric_label_dict) + + def to_dict(self): + """Returns the attributes of the object as a dictionary + + :return: A dict, including all the attributes of the object + :rtype: Dict[str, str] + """ + return {'name': self.name, 'value': self.value} diff --git a/packages/base/ai_api_client_sdk/models/metric_resource.py b/packages/base/ai_api_client_sdk/models/metric_resource.py new file mode 100644 index 0000000..75b1a9a --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metric_resource.py @@ -0,0 +1,51 @@ +from typing import Any, Dict, List + +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_tag import MetricTag + + +class MetricResource: + """The Metric object, defines collection of various metrics/tags/labels related to an execution. + + :param execution_id: ID of the execution + :type execution_id: str + :param metrics: List of the metrics related to the execution, defaults to None + :type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional + :param tags: List of the tags related to the execution, defaults to None + :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional + :param custom_info: List of custom info related to the execution, defaults to None + :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, execution_id: str, metrics: List[Metric] = None, tags: List[MetricTag] = None, + custom_info: List[MetricCustomInfo] = None, **kwargs): + self.execution_id: str = execution_id + self.metrics: List[Metric] = metrics + self.tags: List[MetricTag] = tags + self.custom_info: List[MetricCustomInfo] = custom_info + + def __str__(self): + ret_string = "Metric execution id: " + str(self.execution_id) + ", Metrics: " + for metric in self.metrics: + ret_string += metric.name + ", " + return ret_string[:len(ret_string) - 2] + + @staticmethod + def from_dict(metric_resource_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.metric_resource.MetricResource` object, created from the values + in the dict provided as parameter + + :param metric_resource_dict: Dict which includes the necessary values to create the object + :type metric_resource_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metric_resource.MetricResource` + """ + if metric_resource_dict.get('metrics'): + metric_resource_dict['metrics'] = [Metric.from_dict(m) for m in metric_resource_dict['metrics']] + if metric_resource_dict.get('tags'): + metric_resource_dict['tags'] = [MetricTag.from_dict(mt) for mt in metric_resource_dict['tags']] + if metric_resource_dict.get('custom_info'): + metric_resource_dict['custom_info'] = \ + [MetricCustomInfo.from_dict(mci) for mci in metric_resource_dict['custom_info']] + return MetricResource(**metric_resource_dict) diff --git a/packages/base/ai_api_client_sdk/models/metric_tag.py b/packages/base/ai_api_client_sdk/models/metric_tag.py new file mode 100644 index 0000000..e8a6f52 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metric_tag.py @@ -0,0 +1,24 @@ +from typing import Dict + +from .base_models import NameValue + + +class MetricTag(NameValue): + """The MetricTag object defines a tag as a name-value pair. Refer to + :class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition + """ + + def __str__(self): + return "Metric tag name: " + str(self.name) + "Metric tag value: " + str(self.value) + + @staticmethod + def from_dict(metric_tag_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.metric_tag.MetricTag` object, created from the values in the dict + provided as parameter + + :param metric_tag_dict: Dict which includes the necessary values to create the object + :type metric_tag_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metric_tag.MetricTag` + """ + return MetricTag(**metric_tag_dict) diff --git a/packages/base/ai_api_client_sdk/models/metrics_query_response.py b/packages/base/ai_api_client_sdk/models/metrics_query_response.py new file mode 100644 index 0000000..f2560d1 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/metrics_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .metric_resource import MetricResource + + +class MetricsQueryResponse(QueryResponse): + """The MetricsQueryResponse object defines the response of the metrics query request + :param resources: List of the metrics returned from the server + :type resources: List[class:`ai_api_client_sdk.models.metrics_resource.MetricResource`] + :param count: Total number of the queried metrics + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[MetricResource], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse` + """ + response_dict['resources'] = [MetricResource.from_dict(r) for r in response_dict['resources']] + return MetricsQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/model.py b/packages/base/ai_api_client_sdk/models/model.py new file mode 100644 index 0000000..61f9c21 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/model.py @@ -0,0 +1,64 @@ +from typing import Any, Dict, List + +from ai_api_client_sdk.models.model_version import ModelVersion +from ai_api_client_sdk.models.model_base_data_allowed_scenarios import ModelBaseDataAllowedScenarios + + +class Model: + """The Model object defines a model + :param executable_id: ID of the executable + :type executable_id: str + :param model: Unique name of the model + :type model: str + :param description: Description of the model, defaults to None + :type description: str, optional + :param versions: List of available model versions, defaults to None + :type versions: List[class:`ai_api_client_sdk.models.model_version.ModelVersion`], optional + :param display_name: Display name of the model, defaults to None + :type display_name: str, optional + :param access_type: Access type of the model, defaults to None + :type access_type: str, optional + :param provider: Provider of the model, defaults to None + :type provider: str, optional + :param allowed_scenarios: List of allowed scenarios for the model, defaults to None + :type allowed_scenarios: + List[ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios], optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__( + self, + executable_id: str, + model: str, + description: str = None, + versions: List[ModelVersion] = None, + display_name: str = None, + access_type: str = None, + provider: str = None, + allowed_scenarios: List[ModelBaseDataAllowedScenarios] = None, + **kwargs, + ): + self.executable_id: str = executable_id + self.model: str = model + self.description: str = description + self.versions: List[ModelVersion] = versions + self.display_name: str = display_name + self.access_type: str = access_type + self.provider: str = provider + self.allowed_scenarios: List[ModelBaseDataAllowedScenarios] = allowed_scenarios + + @staticmethod + def from_dict(model_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.model.Model` object, created from the values in the dict + provided as parameter + + :param model_dict: Dict which includes the necessary values to create the object + :type model_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.model.Model` + """ + if model_dict.get("versions"): + model_dict["versions"] = [ + ModelVersion.from_dict(ia) for ia in model_dict["versions"] + ] + return Model(**model_dict) diff --git a/packages/base/ai_api_client_sdk/models/model_base_data_allowed_scenarios.py b/packages/base/ai_api_client_sdk/models/model_base_data_allowed_scenarios.py new file mode 100644 index 0000000..da4270f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/model_base_data_allowed_scenarios.py @@ -0,0 +1,28 @@ + +class ModelBaseDataAllowedScenarios: + """ + This class defines the allowed scenarios for the model base data. + """ + + def __init__(self, scenario_id: str, executable_id: str): + """ + :param scenario_id: ID of the scenario + :type scenario_id: str + :param executable_id: ID of the executable + :type executable_id: str + """ + self._scenario_id: str = scenario_id + self._executable_id: str = executable_id + + @staticmethod + def from_dict(allowed_scenarios_dict: dict[str, any]): + """ + Returns a :class:`ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios` + object, created from the values in the dict provided as parameter + + :param allowed_scenarios_dict: Dict which includes the necessary values to create the object + :type allowed_scenarios_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios` + """ + return ModelBaseDataAllowedScenarios(**allowed_scenarios_dict) diff --git a/packages/base/ai_api_client_sdk/models/model_query_response.py b/packages/base/ai_api_client_sdk/models/model_query_response.py new file mode 100644 index 0000000..856763f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/model_query_response.py @@ -0,0 +1,32 @@ +from typing import Any, Dict, List + +from ai_api_client_sdk.models.base_models import QueryResponse +from ai_api_client_sdk.models.model import Model + + +class ModelQueryResponse(QueryResponse): + """The ModelQueryResponse object defines the response of the model query request + :param resources: List of the models returned from the server + :type resources: List[class:`ai_api_client_sdk.models.executable.Model`] + :param count: Total number of the queried models + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, resources: List[Model], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse` + """ + response_dict["resources"] = [ + Model.from_dict(r) for r in response_dict["resources"] + ] + return ModelQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/model_version.py b/packages/base/ai_api_client_sdk/models/model_version.py new file mode 100644 index 0000000..da78a2a --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/model_version.py @@ -0,0 +1,81 @@ +from datetime import datetime +from typing import Any, Dict, Optional, List + + +class ModelVersion: + """The ModelVersion object defines a version for a model + :param name: Name of the model version + :type name: str + :param is_latest: True if model version is latest, otherwise false + :type is_latest: bool + :param deprecated: True if model version is deprecated, otherwise false + :type deprecated: bool + :param retirement_date: Retirement date of the model version, defaults to None + :type retirement_date: datetime, optional + :param context_length: Context length of the model version, defaults to None + :type context_length: int, optional + :param input_types: Input types supported by the model version, defaults to None + :type input_types: List[str], optional + :param capabilities: Capabilities of the model version, defaults to None + :type capabilities: List[str], optional + :param metadata: Metadata of the model version, defaults to None + :type metadata: List[Dict[str, str]], optional + :param cost: Cost of the model version, defaults to None + :type cost: List[Dict[str, str]], optional + :param suggested_replacements: Suggested replacements for the model version, defaults to None + :type suggested_replacements: List[str], optional + :param streaming_supported: True if streaming is supported, otherwise false + :type streaming_supported: bool, optional + :param orchestration_capabilities: Orchestration capabilities of the model version, defaults to None + :type orchestration_capabilities: List[str], optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__( + self, + name: str, + is_latest: bool, + deprecated: bool, + retirement_date: Optional[datetime] = None, + context_length: Optional[int] = None, + input_types: Optional[List[str]] = None, + capabilities: Optional[List[str]] = None, + metadata: Optional[Dict[str, str]] = None, + cost: Optional[Dict[str, str]] = None, + suggested_replacements: Optional[List[str]] = None, + streaming_supported: Optional[bool] = None, + orchestration_capabilities: Optional[List[str]] = None, + **kwargs, + ): + self.name: str = name + self.is_latest: bool = is_latest + self.deprecated: bool = deprecated + self.retirement_date: Optional[datetime] = retirement_date + self.context_length: Optional[int] = context_length + self.input_types: Optional[List[str]] = input_types + self.capabilities: Optional[List[str]] = capabilities + self.metadata: Optional[Dict[str, str]] = metadata + self.cost: Optional[Dict[str, str]] = cost + self.suggested_replacements: Optional[List[str]] = suggested_replacements + self.streaming_supported: Optional[bool] = streaming_supported + self.orchestration_capabilities: Optional[List[str]] = orchestration_capabilities + + def __eq__(self, other): + if not isinstance(other, ModelVersion): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + @staticmethod + def from_dict(model_version_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.model_version.ModelVersion` object, created from the values in the dict + provided as parameter + + :param model_version_dict: Dict which includes the necessary values to create the object + :type model_version_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.model_version.ModelVersion` + """ + return ModelVersion(**model_version_dict) diff --git a/packages/base/ai_api_client_sdk/models/output_artifact.py b/packages/base/ai_api_client_sdk/models/output_artifact.py new file mode 100644 index 0000000..441dc66 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/output_artifact.py @@ -0,0 +1,45 @@ +from typing import Any, Dict, List + +from ai_api_client_sdk.models.label import Label + + +class OutputArtifact: + """The OutputArtifact object defines the output artifact specified in the executable definition. + :param name: name of artifact + :type name: str + :param kind: kind of artifact (Dataset, Model, ResultSet) + :type kind: str + :param description: description of artifact + :type description: str + :param labels: labels for artifact + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + """ + def __init__(self, name: str, kind: str = None, description: str = None, labels: List[Label] = None): + self.name: str = name + self.kind: str = kind + self.description: str = description + self.labels: List[Label] = labels + + def __str__(self): + return "Output artifact name: " + str(self.name) + ", Output artifact kind: " + str(self.kind) + \ + ", Output artifact description: " + str(self.description) + + def __eq__(self, other): + if not isinstance(other, OutputArtifact): + return False + return self.name == other.name and self.kind == other.kind and self.description == other.description and \ + self.labels == other.labels + + @staticmethod + def from_dict(output_artifact_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.output_artifact.OutputArtifact` object, created from the values + in the dict provided as parameter + + :param output_artifact_dict: Dict which includes the necessary values to create the object + :type output_artifact_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.output_artifact.OutputArtifact` + """ + if output_artifact_dict.get('labels'): + output_artifact_dict['labels'] = [Label.from_dict(l) for l in output_artifact_dict['labels']] + return OutputArtifact(**output_artifact_dict) diff --git a/packages/base/ai_api_client_sdk/models/parameter.py b/packages/base/ai_api_client_sdk/models/parameter.py new file mode 100644 index 0000000..e7a2a1a --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/parameter.py @@ -0,0 +1,48 @@ +from enum import Enum +from typing import Any, Dict + + +class Parameter: + """The Parameter object defines the parameter specified in the executable definition. + + :param name: name of the parameter + :type name: str + :param type: Type of the parameter + :type type: class:`ai_api_client_sdk.models.parameter.Parameter.Type` + :param description: description for parameter + :type description: str + :param default: default value for parameter + :type default: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + class Type(Enum): + STRING = 'string' + + def __init__(self, name: str, type: Type, description: str = None, default: str = None, **kwargs): + self.name: str = name + self.type: Parameter.Type = type # pylint: disable=used-before-assignment + self.description: str = description + self.default: str = default + + def __eq__(self, other): + if not isinstance(other, Parameter): + return False + return self.name == other.name and self.type == other.type + + def __str__(self): + return "Parameter name: " + str(self.name) + ", Parameter type: " + str(self.type.value) + ", Parameter " \ + "description: " + \ + str(self.description) + ", Parameter default value: " + str(self.default) + + @staticmethod + def from_dict(parameter_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.parameter.Parameter` object, created from the values in the dict + provided as parameter + + :param parameter_dict: Dict which includes the necessary values to create the object + :type parameter_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.parameter.Parameter` + """ + parameter_dict['type'] = Parameter.Type(parameter_dict['type']) + return Parameter(**parameter_dict) diff --git a/packages/base/ai_api_client_sdk/models/parameter_binding.py b/packages/base/ai_api_client_sdk/models/parameter_binding.py new file mode 100644 index 0000000..4c3e145 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/parameter_binding.py @@ -0,0 +1,24 @@ +from typing import Dict + +from .base_models import KeyValue + + +class ParameterBinding(KeyValue): + """The ParameterBinding object defines the input artifact specified in the configuration, as a key-value pair. Refer + to :class:`ai_api_client_sdk.models.base_models.KeyValue`, for the object definition + """ + + def __str__(self): + return "Parameter binding key: " + str(self.key) + "Parameter binding value: " + str(self.value) + + @staticmethod + def from_dict(parameter_binding_dict: Dict[str, str]): + """Returns a :class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding` object, created from + the values in the dict provided as parameter + + :param parameter_binding_dict: Dict which includes the necessary values to create the object + :type parameter_binding_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding` + """ + return ParameterBinding(**parameter_binding_dict) diff --git a/packages/base/ai_api_client_sdk/models/resource_group.py b/packages/base/ai_api_client_sdk/models/resource_group.py new file mode 100644 index 0000000..4ca4d78 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/resource_group.py @@ -0,0 +1,48 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from .label import Label +from .resource_group_status import ResourceGroupStatus + + +class ResourceGroup: + """ResourceGroup represents the resource group. + + :param resource_group_id: The resource_group_id of this ResourceGroup. + :type resource_group_id: str + :param labels: The labels of this ResourceGroup. + :type labels: ResourceGroupLabels + :param status: The status of this ResourceGroup. + :type status: str + :param created_at: Time when the resource group was created + :type created_at: datetime + """ + def __init__(self, resource_group_id: str = None, labels: List[Label] = None, status: ResourceGroupStatus = None, + created_at: datetime = None, *args, **kwargs): + self.resource_group_id: str = resource_group_id + self.labels: List[Label] = labels + self.status: ResourceGroupStatus = status + self.created_at: datetime = created_at + + def __str__(self): + return "Resource group id: " + str(self.resource_group_id) + + @staticmethod + def from_dict(resource_group_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.resource_group.ResourceGroup` object, created + from the values in the dict provided as parameter + + :param resource_group_dict: Dict which includes the necessary values to create the object + :type resource_group_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup` + """ + if 'resource_group_status' in resource_group_dict: + resource_group_dict['resource_group_status'] = \ + ResourceGroupStatus(resource_group_dict['resource_group_status']) + if 'labels' in resource_group_dict: + resource_group_dict['labels'] = [Label.from_dict(l) for l in resource_group_dict['labels']] + if 'created_at' in resource_group_dict: + resource_group_dict['created_at'] = parse_datetime(resource_group_dict['created_at']) + return ResourceGroup(**resource_group_dict) diff --git a/packages/base/ai_api_client_sdk/models/resource_group_query_response.py b/packages/base/ai_api_client_sdk/models/resource_group_query_response.py new file mode 100644 index 0000000..f3917ef --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/resource_group_query_response.py @@ -0,0 +1,31 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .resource_group import ResourceGroup + + +class ResourceGroupQueryResponse(QueryResponse): + """The ResourceGroupQueryResponse object defines the response of the resourceGroups query request + :param resources: List of the resource groups returned from the server + :type resources: List[class:`ai_core_sdk.models.resource_group.ResourceGroup`] + :param count: Total number of the queried docker registry secrets + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, resources: List[ResourceGroup], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + """ + response_dict['resources'] = [ResourceGroup.from_dict(r) for r in response_dict['resources']] + return ResourceGroupQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/resource_group_status.py b/packages/base/ai_api_client_sdk/models/resource_group_status.py new file mode 100644 index 0000000..1407f09 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/resource_group_status.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ResourceGroupStatus(Enum): + """ResourceGroupStatus is an Enum defining the valid values of the status of a resource group + """ + ERROR = 'ERROR' + PROVISIONED = 'PROVISIONED' + PROVISIONING = 'PROVISIONING' diff --git a/packages/base/ai_api_client_sdk/models/scenario.py b/packages/base/ai_api_client_sdk/models/scenario.py new file mode 100644 index 0000000..e503222 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/scenario.py @@ -0,0 +1,68 @@ +from datetime import datetime +from typing import Any, Dict, List + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.helpers.llm_helper import check_if_llm_scenario +from ai_api_client_sdk.models.label import Label + + +class Scenario: + """The Scenario object defines a scenario + :param id: ID of the scenario + :type id: str + :param created_at: Time when the scenario was created + :type created_at: datetime + :param modified_at: Time when the scenario was last modified + :type modified_at: datetime + :param name: Name of the scenario + :type name: str + :param description: Description of the scenario, defaults to None + :type description: str, optional + :param labels: List of the labels of the scenario, defaults to None + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, id: str, created_at: datetime, modified_at: datetime, name: str, description: str = None, + labels: List[Label] = None, **kwargs): + self.id: str = id + self.name: str = name + self.description: str = description + self.labels: List[Label] = labels + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + + def __eq__(self, other): + if not isinstance(other, Scenario): + return False + for k in self.__dict__.keys(): + if getattr(self, k) != getattr(other, k): + return False + return True + + def __str__(self): + return "Scenario id: " + str(self.id) + ", Scenario description: " + str(self.description) + + @staticmethod + def from_dict(scenario_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.scenario.Scenario` object, created from the values in the dict + provided as parameter + + :param scenario_dict: Dict which includes the necessary values to create the object + :type scenario_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.scenario.Scenario` + """ + scenario_dict['created_at'] = parse_datetime(scenario_dict['created_at']) + scenario_dict['modified_at'] = parse_datetime(scenario_dict['modified_at']) + if scenario_dict.get('labels'): + scenario_dict['labels'] = [Label.from_dict(l) for l in scenario_dict['labels']] + return Scenario(**scenario_dict) + + def is_llm_scenario(self): + """Returns if the scenario is a LLM scenario or not + + :return: True if scenario is llm scenario, False otherwise + :rtype: bool + """ + return check_if_llm_scenario(scenario=self) diff --git a/packages/base/ai_api_client_sdk/models/scenario_query_response.py b/packages/base/ai_api_client_sdk/models/scenario_query_response.py new file mode 100644 index 0000000..708e9d0 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/scenario_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .scenario import Scenario + + +class ScenarioQueryResponse(QueryResponse): + """The ScenarioQueryResponse object defines the response of the scenario query request + :param resources: List of the scenarios returned from the server + :type resources: List[class:`ai_api_client_sdk.models.scenario.Scenario`] + :param count: Total number of the queried scenarios + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Scenario], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` + """ + response_dict['resources'] = [Scenario.from_dict(r) for r in response_dict['resources']] + return ScenarioQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/models/status.py b/packages/base/ai_api_client_sdk/models/status.py new file mode 100644 index 0000000..7c65882 --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/status.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class Status(Enum): + """Status is an Enum defining the valid values of the status of an execution/deployment + """ + PENDING = 'PENDING' + RUNNING = 'RUNNING' + COMPLETED = 'COMPLETED' + DEAD = 'DEAD' + STOPPING = 'STOPPING' + STOPPED = 'STOPPED' + UNKNOWN = 'UNKNOWN' + + +class ScheduleStatus(Enum): + """Enum defining the status values for execution schedules + """ + ACTIVE = 'ACTIVE' + INACTIVE = 'INACTIVE' diff --git a/packages/base/ai_api_client_sdk/models/target_status.py b/packages/base/ai_api_client_sdk/models/target_status.py new file mode 100644 index 0000000..2a986db --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/target_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class TargetStatus(Enum): + """TargetStatus is an Enum defining the valid values of the target status of an execution/deployment + """ + RUNNING = 'RUNNING' + COMPLETED = 'COMPLETED' + STOPPED = 'STOPPED' + DELETED = 'DELETED' diff --git a/packages/base/ai_api_client_sdk/models/version.py b/packages/base/ai_api_client_sdk/models/version.py new file mode 100644 index 0000000..3d356da --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/version.py @@ -0,0 +1,44 @@ +from datetime import datetime +from typing import Any, Dict + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime + + +class Version: + """The Version object defines a scenario + :param id: ID of the version + :type id: str + :param scenario_id: ID of the scenario the version belongs to + :type scenario_id: str + :param created_at: Time when the scenario was created + :type created_at: datetime + :param modified_at: Time when the scenario was last modified + :type modified_at: datetime + :param description: Description of the scenario, defaults to None + :type description: str, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, id: str, scenario_id: str, created_at: datetime, modified_at: datetime, description: str = None, + **kwargs): + self.id: str = id + self.scenario_id: str = scenario_id + self.description: str = description + self.created_at: datetime = created_at + self.modified_at: datetime = modified_at + + def __str__(self): + return "Version id: " + str(self.id) + + @staticmethod + def from_dict(version_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.version.Version` object, created from the values in the dict + provided as parameter + + :param version_dict: Dict which includes the necessary values to create the object + :type version_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.version.Version` + """ + version_dict['created_at'] = parse_datetime(version_dict['created_at']) + version_dict['modified_at'] = parse_datetime(version_dict['modified_at']) + return Version(**version_dict) diff --git a/packages/base/ai_api_client_sdk/models/version_list.py b/packages/base/ai_api_client_sdk/models/version_list.py new file mode 100644 index 0000000..a2be00f --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/version_list.py @@ -0,0 +1,32 @@ +from typing import Dict, List + +from ai_api_client_sdk.models.api_version import APIVersion + + +class VersionList: + """The VersionList object, is a list of API version descriptions + + :param versions: A list of objects describing the API versions, defaults to None + :type versions: class:`ai_api_client_sdk.models.api_version.APIVersion`, optional + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, versions: List[APIVersion] = None): + self.versions: List[APIVersion] = versions + + @staticmethod + def from_dict(version_list_dict: Dict[str, List[Dict[str, str]]]): + """Returns a :class:`ai_api_client_sdk.models.version_list.VersionList` object, created from the + values in the dict provided as parameter + + :param version_list_dict: Dict which includes the necessary values to create the object + :type version_list_dict: Dict[str, List[Dict[str, str]] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.version_list.VersionList` + """ + if 'versions' in version_list_dict: + for i in range(len(version_list_dict['versions'])): + version_list_dict['versions'][i] = APIVersion.from_dict(version_list_dict['versions'][i]) + return VersionList(**version_list_dict) + + def __str__(self): + return str(self.__dict__) diff --git a/packages/base/ai_api_client_sdk/models/version_query_response.py b/packages/base/ai_api_client_sdk/models/version_query_response.py new file mode 100644 index 0000000..26e573d --- /dev/null +++ b/packages/base/ai_api_client_sdk/models/version_query_response.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, List + +from .base_models import QueryResponse +from .version import Version + + +class VersionQueryResponse(QueryResponse): + """The VersionQueryResponse object defines the response of the version query request + :param resources: List of the versions returned from the server + :type resources: List[class:`ai_api_client_sdk.models.version.Version`] + :param count: Total number of the queried versions + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Version], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a :class:`ai_api_client_sdk.models.version_query_response.VersionQueryResponse` object, created + from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_api_client_sdk.models.version_query_response.VersionQueryResponse` + """ + response_dict['resources'] = [Version.from_dict(r) for r in response_dict['resources']] + return VersionQueryResponse(**response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/__init__.py b/packages/base/ai_api_client_sdk/resource_clients/__init__.py new file mode 100644 index 0000000..91b9387 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/__init__.py @@ -0,0 +1,12 @@ +from .artifact_client import ArtifactClient +from .configuration_client import ConfigurationClient +from .deployment_client import DeploymentClient +from .executable_client import ExecutableClient +from .execution_client import ExecutionClient +from .execution_schedule_client import ExecutionScheduleClient +from .healthz_client import HealthzClient +from .meta_client import MetaClient +from .metrics_client import MetricsClient +from .model_client import ModelClient +from .resource_groups_client import ResourceGroupsClient +from .scenario_client import ScenarioClient diff --git a/packages/base/ai_api_client_sdk/resource_clients/artifact_client.py b/packages/base/ai_api_client_sdk/resource_clients/artifact_client.py new file mode 100644 index 0000000..4f0c9ed --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/artifact_client.py @@ -0,0 +1,162 @@ +from typing import List + +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.artifact_create_response import ArtifactCreateResponse +from ai_api_client_sdk.models.artifact_query_response import ArtifactQueryResponse +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ArtifactClient(BaseClient): + """ArtifactClient is a class implemented for interacting with the artifact related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def create(self, name: str, kind: Artifact.Kind, url: str, scenario_id: str, description: str = None, + labels: List[Label] = None, resource_group: str = None) -> ArtifactCreateResponse: + """Creates an artifact. + + :param name: Name of the artifact + :type name: str + :param kind: Kind of the artifact + :type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind` + :param url: URL of the artifact + :type url: str + :param scenario_id: ID of the scenario which the artifact should belong to + :type scenario_id: str + :param description: Description of the artifact, defaults to None + :type description: str, optional + :param labels: List of the labels of the artifact, defaults to None + :type labels: List[class:`ai_api_client_sdk.models.label.Label`] + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse` + """ + body = { + 'name': name, + 'kind': kind.value, + 'url': url, + 'scenario_id': scenario_id + } + if description: + body['description'] = description + if labels: + body['labels'] = [l.to_dict() for l in labels] + response_dict = self.rest_client.post(path='/artifacts', body=body, resource_group=resource_group) + return ArtifactCreateResponse.from_dict(response_dict) + + def get(self, artifact_id: str, expand: str = None, resource_group: str = None) -> Artifact: + """Retrieves the artifact from the server. + + :param artifact_id: ID of the artifact to be retrieved + :type artifact_id: str + :param expand: Entity whose details to be displayed in the response, defaults to None + :type expand: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved artifact + :rtype: class:`ai_api_client_sdk.models.artifact.Artifact` + """ + params = self._form_query_params(expand=expand) + artifact_dict = self.rest_client.get(path=f'/artifacts/{artifact_id}', params=params, + resource_group=resource_group) + return Artifact.from_dict(artifact_dict) + + def query(self, scenario_id: str = None, execution_id: str = None, name: str = None, kind: Artifact.Kind = None, + artifact_label_selector: List[str] = None, top: int = None, skip: int = None, search: str = None, + search_case_insensitive: bool = None, expand: str = None, + resource_group: str = None) -> ArtifactQueryResponse: + """Queries the artifacts. + + :param scenario_id: ID of the scenario the artifacts should belong to, defaults to None + :type scenario_id: str, optional + :param execution_id: ID of the execution the artifact should be resulted from, defaults to None + :type execution_id: str, optional + :param name: Name of the artifact(s) to be retrieved, defaults to None + :type name: str, optional + :param kind: Kind of the artifacts to be retrieved, defaults to None + :type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`, optional + :param artifact_label_selector: Query the artifacts based on their labels in the form of "key=value" or + "key!=value" separated by commas, defaults to None + :type artifact_label_selector: list, optional + :param top: Number of artifacts to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of artifacts to be skipped, from the list of the queried artifacts, defaults to None + :type skip: int, optional + :param search: Generic search term to be looked for in various attributes of artifacts, defaults to None + :type search: str, optional + :param search_case_insensitive: Indicates whether the search should be case insensitive + :type search_case_insensitive: bool, optional + :param expand: Entity whose details to be displayed in the response, defaults to None + :type expand: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse` + """ + params = self._form_query_params(scenario_id=scenario_id, execution_id=execution_id, name=name, top=top, + skip=skip, artifact_label_selector=artifact_label_selector, search=search, + search_case_insensitive=search_case_insensitive, expand=expand, + kind=kind.value if kind else None) + response_dict = self.rest_client.get(path='/artifacts', params=params, resource_group=resource_group) + return ArtifactQueryResponse.from_dict(response_dict) + + def count(self, scenario_id: str = None, execution_id: str = None, name: str = None, kind: Artifact.Kind = None, + artifact_label_selector: List[str] = None, resource_group: str = None) -> int: + """Counts the artifacts. + + :param scenario_id: ID of the scenario the artifacts should belong to, defaults to None + :type scenario_id: str, optional + :param execution_id: ID of the execution the artifact should be resulted from, defaults to None + :type execution_id: str, optional + :param name: Name of the artifact(s) to be retrieved, defaults to None + :type name: str, optional + :param kind: Kind of the artifacts to be retrieved, defaults to None + :type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`, optional + :param artifact_label_selector: list of the label selector strings in the form of "key=value" or "key!=value", to filter + the artifacts with respect to their labels, defaults to None + :type artifact_label_selector: List[str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: int + """ + params = self._form_query_params(scenario_id=scenario_id, execution_id=execution_id, name=name, + kind=kind.value if kind else None, + artifact_label_selector=artifact_label_selector) + return self.rest_client.get(path='/artifacts/$count', params=params, resource_group=resource_group) diff --git a/packages/base/ai_api_client_sdk/resource_clients/base_client.py b/packages/base/ai_api_client_sdk/resource_clients/base_client.py new file mode 100644 index 0000000..b816bbb --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/base_client.py @@ -0,0 +1,79 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Dict + +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT +from ai_api_client_sdk.helpers.rest_client import RestClient + + +class BaseClient: + """BaseClient defines the interface for the resource clients. + + :param rest_client: the client used to make calls to the server + :type rest_client: class:`ai_api_client_sdk.helpers.rest_client.RestClient` + """ + + def __init__(self, rest_client: RestClient): + self.rest_client: RestClient = rest_client + + def create(self, *args, **kwargs): + """Creates the relevant resource. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def delete(self, *args, **kwargs): + """Deletes the relevant resource. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def get(self, *args, **kwargs): + """Retrieves the relevant resource. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def modify(self, *args, **kwargs): + """Modifies the relevant resource. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def bulk_modify(self, *args, **kwargs): + """Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def query(self, *args, **kwargs): + """Queries the relevant resources. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def count(self, *args, **kwargs): + """Counts the relevant resources. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + def query_logs(self, *args, **kwargs): + """Queries the relevant logs. Will be implemented by the respective resource clients""" + raise NotImplementedError() + + @staticmethod + def _form_query_params(**kwargs) -> Dict[str, Any]: + """Creates a params dict, from the keyword arguments. Reforms the list parameters into the expected form by the + server + + :param kwargs: keyword arguments which are to be reformed to a dict. + :return: A dict, defining the parameters + :rtype: Dict[str, Any] + """ + params = {} + for k, v in kwargs.items(): + if v: + if isinstance(v, list): + v = ','.join(v) + elif isinstance(v, datetime): + v = v.strftime(DATETIME_FORMAT) + elif isinstance(v, Enum): + v = v.value + params[k] = v + if 'search' in params: + params['$search'] = params['search'] + del params['search'] + if 'expand' in params: + params['$expand'] = params['expand'] + del params['expand'] + if 'select' in params: + params['$select'] = params['select'] + del params['select'] + return params if len(params) > 0 else None diff --git a/packages/base/ai_api_client_sdk/resource_clients/configuration_client.py b/packages/base/ai_api_client_sdk/resource_clients/configuration_client.py new file mode 100644 index 0000000..838ba89 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/configuration_client.py @@ -0,0 +1,147 @@ +from typing import List + +from ai_api_client_sdk.models.configuration import Configuration +from ai_api_client_sdk.models.configuration_create_response import ConfigurationCreateResponse +from ai_api_client_sdk.models.configuration_query_response import ConfigurationQueryResponse +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ConfigurationClient(BaseClient): + """ConfigurationClient is a class implemented for interacting with the configuration related endpoints of the + server. It implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def create(self, name: str, scenario_id: str, executable_id: str, parameter_bindings: List[ParameterBinding] = None, + input_artifact_bindings: List[InputArtifactBinding] = None, + resource_group: str = None) -> ConfigurationCreateResponse: + """Creates a configuration. + + :param name: Name of the configuration + :type name: str + :param scenario_id: ID of the scenario which the configuration should belong to + :type scenario_id: str + :param executable_id: ID of the executable, which should be configured + :type executable_id: str + :param parameter_bindings: List of the input parameters, defaults to None + :type parameter_bindings: List[class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding`], optional + :param input_artifact_bindings: List of the input artifacts which are to be used by the executable, + defaults to None + :type input_artifact_bindings: + List[class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding`], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse` + """ + body = { + 'name': name, + 'scenario_id': scenario_id, + 'executable_id': executable_id + } + if parameter_bindings: + body['parameter_bindings'] = [pb.to_dict() for pb in parameter_bindings] + if input_artifact_bindings: + body['input_artifact_bindings'] = [iab.to_dict() for iab in input_artifact_bindings] + response_dict = self.rest_client.post(path='/configurations', body=body, resource_group=resource_group) + return ConfigurationCreateResponse.from_dict(response_dict) + + def get(self, configuration_id: str, expand: str = None, resource_group: str = None) -> Configuration: + """Retrieves the configuration from the server. + + :param configuration_id: ID of the configuration to be retrieved + :type configuration_id: str + :param expand: Entity whose details to be displayed in the response, defaults to None + :type expand: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved configuration + :rtype: class:`ai_api_client_sdk.models.configuration.Configuration` + """ + params = self._form_query_params(expand=expand) + configuration_dict = self.rest_client.get(path=f'/configurations/{configuration_id}', params=params, + resource_group=resource_group) + return Configuration.from_dict(configuration_dict) + + def query(self, scenario_id: str = None, executable_ids: List[str] = None, top: int = None, skip: int = None, + search: str = None, search_case_insensitive: bool = None, expand: str = None, + resource_group: str = None) -> ConfigurationQueryResponse: + """Queries the configurations. + + :param scenario_id: ID of the scenario the configurations should belong to, defaults to None + :type scenario_id: str, optional + :param executable_ids: IDs of the executables the configurations should have configured, defaults to None + :type executable_ids: List[str], optional + :param top: Number of configurations to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of configurations to be skipped, from the list of the queried configurations, defaults to + None + :type skip: int, optional + :param search: Generic search term to be looked for in various attributes of configurations, defaults to None + :type search: str, optional + :param search_case_insensitive: Indicates whether the search should be case insensitive + :type search_case_insensitive: bool, optional + :param expand: Entity whose details to be displayed in the response, defaults to None + :type expand: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse` + """ + params = self._form_query_params(scenario_id=scenario_id, executable_ids=executable_ids, top=top, skip=skip, + search=search, search_case_insensitive=search_case_insensitive, expand=expand) + response_dict = self.rest_client.get(path='/configurations', params=params, resource_group=resource_group) + return ConfigurationQueryResponse.from_dict(response_dict) + + def count(self, scenario_id: str = None, executable_ids: List[str] = None, search: str = None, + resource_group: str = None) -> int: + """Counts the configurations. + + :param scenario_id: ID of the scenario the configurations should belong to, defaults to None + :type scenario_id: str, optional + :param executable_ids: IDs of the executables the configurations should have configured, defaults to None + :type executable_ids: List[str], optional + :param search: Generic search term to be looked for in various attributes of configurations, defaults to None + :type search: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: int + """ + params = self._form_query_params(scenario_id=scenario_id, executable_ids=executable_ids, search=search) + return self.rest_client.get(path='/configurations/$count', params=params, resource_group=resource_group) diff --git a/packages/base/ai_api_client_sdk/resource_clients/deployment_client.py b/packages/base/ai_api_client_sdk/resource_clients/deployment_client.py new file mode 100644 index 0000000..e688b07 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/deployment_client.py @@ -0,0 +1,268 @@ +from datetime import datetime +from typing import List, Union + +from ai_api_client_sdk.exception import AIAPIInvalidInputException +from ai_api_client_sdk.models.base_models import BasicResponse, Order, BasicModifyRequest +from ai_api_client_sdk.models.deployment import Deployment +from ai_api_client_sdk.models.deployment_bulk_modify_response import DeploymentBulkModifyResponse +from ai_api_client_sdk.models.deployment_create_response import DeploymentCreateResponse +from ai_api_client_sdk.models.deployment_get_status_response import DeploymentGetStatusResponse +from ai_api_client_sdk.models.deployment_query_response import DeploymentQueryResponse +from ai_api_client_sdk.models.log_response import LogResponse +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class DeploymentClient(BaseClient): + """DeploymentClient is a class implemented for interacting with the deployment related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def create(self, configuration_id: str, ttl: str = None, resource_group: str = None) -> DeploymentCreateResponse: + """Creates a deployment. + + :param configuration_id: ID of the configuration, that should configure the deployment + :type configuration_id: str + :param ttl: Time to live for deployment and can be none or take a number followed by the unit + (any of following values, minutes(m|M), hours(h|H) or days(d|D)) + :type ttl: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse` + """ + body = {"configuration_id": configuration_id} + if ttl: + body['ttl'] = ttl + response_dict = self.rest_client.post(path='/deployments', body=body, + resource_group=resource_group) + return DeploymentCreateResponse.from_dict(response_dict) + + def delete(self, deployment_id: str, resource_group: str = None) -> BasicResponse: + """Deletes the deployment. + + :param deployment_id: ID of the deployment to be deleted + :type deployment_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(f'/deployments/{deployment_id}', resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def get(self, deployment_id: str, resource_group: str = None, select: str = None) -> \ + Union[Deployment, DeploymentGetStatusResponse]: + """Retrieves the deployment from the server. + + :param deployment_id: ID of the deployment to be retrieved + :type deployment_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :param select: only status supported. Get deployment for a given deployment id and select status + :type select: str, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved deployment + :rtype: class:Union[`ai_api_client_sdk.models.deployment.Deployment`, + `ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse`] + """ + if select and 'status' in select: + param = self._form_query_params(select='status') + deployment_dict = self.rest_client.get(path=f'/deployments/{deployment_id}', params=param, + resource_group=resource_group) + return DeploymentGetStatusResponse.from_dict(deployment_dict) + else: + deployment_dict = self.rest_client.get(path=f'/deployments/{deployment_id}', resource_group=resource_group) + return Deployment.from_dict(deployment_dict) + + def modify(self, deployment_id: str, target_status: TargetStatus = None, configuration_id: str = None, + resource_group: str = None) -> BasicResponse: + """Modifies the deployment, by changing either the target status, or the configuration ID. + + :param deployment_id: ID of the deployment to be modified + :type deployment_id: str + :param target_status: Desired target status of the deployment, defaults to None + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`, optional + :param configuration_id: ID of the new configuration to be used by the deployment, defaults to None + :type configuration_id: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {} + if target_status and configuration_id: + raise AIAPIInvalidInputException( + 'Either target_status or configuration_id should be provided as input, not both') + if target_status: + body['target_status'] = target_status.value + elif configuration_id: + body['configuration_id'] = configuration_id + response_dict = self.rest_client.patch(path=f'/deployments/{deployment_id}', body=body, + resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def query(self, scenario_id: str = None, configuration_id: str = None, executable_ids: List[str] = None, + status: Status = None, top: int = None, skip: int = None, + resource_group: str = None) -> DeploymentQueryResponse: + """Queries the deployments. + + :param scenario_id: ID of the scenario the deployments should belong to, defaults to None + :type scenario_id: str, optional + :param configuration_id: ID of the configuration, the deployments should be configured by, defaults to None + :type configuration_id: str, optional + :param executable_ids: IDs of the executables the deployments should be created from, defaults to None + :type executable_ids: List[str], optional + :param status: Status which the deployments should currently have + :type status: class:`ai_api_client_sdk.models.status.Status`, optional + :param top: Number of deployments to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of deployments to be skipped, from the list of the queried deployments, defaults to None + :type skip: int, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse` + """ + params = self._form_query_params(scenario_id=scenario_id, configuration_id=configuration_id, + executable_ids=executable_ids, status=status.value if status else None, + top=top, skip=skip) + response_dict = self.rest_client.get(path='/deployments', params=params, resource_group=resource_group) + return DeploymentQueryResponse.from_dict(response_dict) + + def count(self, scenario_id: str = None, configuration_id: str = None, executable_ids: List[str] = None, + status: Status = None, resource_group: str = None) -> int: + """Counts the number of deployments. + + :param scenario_id: ID of the scenario, the deployments should belong to, defaults to None + :type scenario_id: str, optional + :param configuration_id: ID of the configuration, the deployments should be configured by, defaults to None + :type configuration_id: str, optional + :param executable_ids: IDs of the executables, the deployments should be created from, defaults to None + :type executable_ids: List[str], optional + :param status: Status which the deployments should currently have + :type status: class:`ai_api_client_sdk.models.status.Status`, optional + :param resource_group: Resource group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: int + """ + params = self._form_query_params(scenario_id=scenario_id, configuration_id=configuration_id, + executable_ids=executable_ids, status=status.value if status else None) + return self.rest_client.get(path='/deployments/$count', params=params, resource_group=resource_group) + + def query_logs(self, deployment_id: str, top: int = None, start: datetime = None, end: datetime = None, + order: Order = None, resource_group: str = None) -> LogResponse: + """Queries the logs of the deployment. + + :param deployment_id: ID of the deployment + :type deployment_id: str + :param top: The max number of entries to return. Defaults to 1000. Limited to 5000 max. + :type top: int + :param start: The start time for the query. Defaults to one hour ago. + :type start: datetime + :param end: The end time for the query. Defaults to now. + :type end: datetime + :param order: Determines the sort order with respect to time. Defaults to asc. + :type order: class:`ai_api_client_sdk.models.base_models.Order` + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: Logs from the execution + :rtype: class:`ai_api_client_sdk.models.log_response.LogResponse` + """ + params = self._form_query_params(top=top, start=start, end=end, order=order) + response_dict = self.rest_client.get(path=f'/deployments/{deployment_id}/logs', params=params, + resource_group=resource_group) + return LogResponse.from_dict(response_dict) + + def bulk_modify(self, deployments: List[BasicModifyRequest], + resource_group: str = None) -> DeploymentBulkModifyResponse: + """Modifies the deployments + :param deployments: List of deployment modify requests + :type deployments: List[DeploymentModifyRequest] + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.deployment_bulk_modify_response.DeploymentBulkModifyResponse` + """ + body = {'deployments': [bmr.to_dict() for bmr in deployments]} + headers = {'Content-Type': 'application/merge-patch+json'} + response_dict = self.rest_client.patch(path='/deployments', body=body, headers=headers, + resource_group=resource_group) + return DeploymentBulkModifyResponse.from_dict(response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/executable_client.py b/packages/base/ai_api_client_sdk/resource_clients/executable_client.py new file mode 100644 index 0000000..ff3f3d7 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/executable_client.py @@ -0,0 +1,60 @@ +from ai_api_client_sdk.models.executable import Executable +from ai_api_client_sdk.models.executable_query_response import ExecutableQueryResponse +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ExecutableClient(BaseClient): + """ExecutableClient is a class implemented for interacting with the executable related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def get(self, scenario_id: str, executable_id: str, resource_group: str = None) -> Executable: + """Retrieves the executable from the server. + + :param scenario_id: ID of the scenario the executable belongs to + :type scenario_id: str + :param executable_id: ID of the executable to be retrieved + :type executable_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved executable + :rtype: class:`ai_api_client_sdk.models.executable.Executable` + """ + executable_dict = self.rest_client.get(path=f'/scenarios/{scenario_id}/executables/{executable_id}', + resource_group=resource_group) + return Executable.from_dict(executable_dict) + + def query(self, scenario_id: str, version_id: str = None, resource_group: str = None) -> ExecutableQueryResponse: + """Queries the executables. + + :param scenario_id: ID of the scenario the executables should belong to, defaults to None + :type scenario_id: str, optional + :param version_id: ID of the version, the executions should have, defaults to None + :type version_id: str, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse` + """ + params = self._form_query_params(version_id=version_id) + response_dict = self.rest_client.get(path=f'/scenarios/{scenario_id}/executables', params=params, + resource_group=resource_group) + return ExecutableQueryResponse.from_dict(response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/execution_client.py b/packages/base/ai_api_client_sdk/resource_clients/execution_client.py new file mode 100644 index 0000000..07c5d4b --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/execution_client.py @@ -0,0 +1,261 @@ +from datetime import datetime +from typing import List, Union + +from ai_api_client_sdk.models.base_models import BasicResponse, Order, BasicModifyRequest +from ai_api_client_sdk.models.execution import Execution +from ai_api_client_sdk.models.execution_bulk_modify_response import ExecutionBulkModifyResponse +from ai_api_client_sdk.models.execution_create_response import ExecutionCreateResponse +from ai_api_client_sdk.models.execution_get_status_response import ExecutionGetStatusResponse +from ai_api_client_sdk.models.execution_query_response import ExecutionQueryResponse +from ai_api_client_sdk.models.log_response import LogResponse +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ExecutionClient(BaseClient): + """ExecutionClient is a class implemented for interacting with the execution related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def create(self, configuration_id: str, resource_group: str = None) -> ExecutionCreateResponse: + """Creates an execution. + + :param configuration_id: ID of the configuration, that should configure the execution + :type configuration_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse` + """ + body = {"configuration_id": configuration_id} + response_dict = self.rest_client.post(path='/executions', body=body, + resource_group=resource_group) + return ExecutionCreateResponse.from_dict(response_dict) + + def delete(self, execution_id: str, resource_group: str = None) -> BasicResponse: + """Deletes the execution. + + :param execution_id: ID of the execution to be deleted + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'/executions/{execution_id}', resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def get(self, execution_id: str, resource_group: str = None, select: str = None) -> \ + Union[Execution, ExecutionGetStatusResponse]: + """Retrieves the execution from the server. + + :param execution_id: ID of the execution to be retrieved + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :param select: only status supported. Get execution for a given execution id and select status + :type select: str, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved execution + :rtype: class:Union[`ai_api_client_sdk.models.execution.Execution`, + `ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse`] + """ + if select and 'status' in select: + param = self._form_query_params(select='status') + execution_dict = self.rest_client.get(path=f'/executions/{execution_id}', params=param, + resource_group=resource_group) + return ExecutionGetStatusResponse.from_dict(execution_dict) + else: + execution_dict = self.rest_client.get(path=f'/executions/{execution_id}', resource_group=resource_group) + return Execution.from_dict(execution_dict) + + def modify(self, execution_id: str, target_status: TargetStatus, resource_group: str = None) -> BasicResponse: + """Modifies the execution, by changing the target status. + + :param execution_id: ID of the execution to be modified + :type execution_id: str + :param target_status: Desired target status of the execution, defaults to None + :type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {'target_status': target_status.value} + response_dict = self.rest_client.patch(path=f'/executions/{execution_id}', body=body, + resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def query(self, scenario_id: str = None, configuration_id: str = None, executable_ids: List[str] = None, + execution_schedule_id: str = None, status: Status = None, top: int = None, skip: int = None, + resource_group: str = None) -> ExecutionQueryResponse: + """Queries the executions. + + :param scenario_id: ID of the scenario the executions should belong to, defaults to None + :type scenario_id: str, optional + :param configuration_id: ID of the configuration, the executions should be configured by, defaults to None + :type configuration_id: str, optional + :param executable_ids: IDs of the executables the executions should be created from, defaults to None + :type executable_ids: List[str], optional + :param execution_schedule_id: ID of the execution schedule, defaults to None + :type execution_schedule_id: str, optional + :param status: Status which the executions should currently have + :type status: class:`ai_api_client_sdk.models.status.Status`, optional + :param top: Number of executions to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of executions to be skipped, from the list of the queried executions, defaults to None + :type skip: int, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse` + """ + params = self._form_query_params(scenario_id=scenario_id, configuration_id=configuration_id, + executable_ids=executable_ids, execution_schedule_id=execution_schedule_id, + status=status.value if status else None, + top=top, skip=skip) + response_dict = self.rest_client.get(path='/executions', params=params, resource_group=resource_group) + return ExecutionQueryResponse.from_dict(response_dict) + + def count(self, scenario_id: str = None, configuration_id: str = None, executable_ids: List[str] = None, + execution_schedule_id: str = None, status: Status = None, resource_group: str = None) -> int: + """Counts the number of executions. + + :param scenario_id: ID of the scenario, the executions should belong to, defaults to None + :type scenario_id: str, optional + :param configuration_id: ID of the configuration, the executions should be configured by, defaults to None + :type configuration_id: str, optional + :param executable_ids: IDs of the executables, the executions should be created from, defaults to None + :type executable_ids: List[str], optional + :param execution_schedule_id: ID of the execution schedule, defaults to None + :type execution_schedule_id: str, optional + :param status: Status which the executions should currently have + :type status: class:`ai_api_client_sdk.models.status.Status`, optional + :param resource_group: Resource group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: int + """ + params = self._form_query_params(scenario_id=scenario_id, configuration_id=configuration_id, + executable_ids=executable_ids, execution_schedule_id=execution_schedule_id, + status=status.value if status else None) + return self.rest_client.get(path='/executions/$count', params=params, resource_group=resource_group) + + def query_logs(self, execution_id: str, top: int = None, start: datetime = None, end: datetime = None, + order: Order = None, resource_group: str = None) -> LogResponse: + """Queries the logs of the execution. + + :param execution_id: ID of the execution + :type execution_id: str + :param top: The max number of entries to return. Defaults to 1000. Limited to 5000 max. + :type top: int + :param start: The start time for the query. Defaults to one hour ago. + :type start: datetime + :param end: The end time for the query. Defaults to now. + :type end: datetime + :param order: Determines the sort order with respect to time. Defaults to asc. + :type order: class:`ai_api_client_sdk.models.base_models.Order` + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: Logs from the execution + :rtype: class:`ai_api_client_sdk.models.log_response.LogResponse` + """ + params = self._form_query_params( + top=top, start=start, end=end, order=order) + response_dict = self.rest_client.get(path=f'/executions/{execution_id}/logs', params=params, + resource_group=resource_group) + return LogResponse.from_dict(response_dict) + + def bulk_modify(self, executions: List[BasicModifyRequest], + resource_group: str = None) -> ExecutionBulkModifyResponse: + """Modifies the executions + :param executions: List of execution modify requests + :type executions: List[ExecutionModifyRequest] + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse` + """ + body = {'executions': [bmr.to_dict() for bmr in executions]} + headers = {'Content-Type': 'application/merge-patch+json'} + response_dict = self.rest_client.patch(path='/executions', body=body, headers=headers, + resource_group=resource_group) + return ExecutionBulkModifyResponse.from_dict(response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/execution_schedule_client.py b/packages/base/ai_api_client_sdk/resource_clients/execution_schedule_client.py new file mode 100644 index 0000000..ab3b92c --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/execution_schedule_client.py @@ -0,0 +1,212 @@ +from datetime import datetime + +from ai_api_client_sdk.exception import AIAPIInvalidInputException +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT +from ai_api_client_sdk.models.base_models import BasicResponse +from ai_api_client_sdk.models.execution_schedule import ExecutionSchedule +from ai_api_client_sdk.models.execution_schedule_create_response import ExecutionScheduleCreateResponse +from ai_api_client_sdk.models.execution_schedule_query_response import ExecutionScheduleQueryResponse +from ai_api_client_sdk.models.status import ScheduleStatus +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ExecutionScheduleClient(BaseClient): + """ExecutionScheduleClient is a class implemented for interacting with the execution schedules related endpoints of + the server. It implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def create(self, name: str, cron: str, configuration_id: str, start: datetime = None, end: datetime = None, + resource_group: str = None) -> ExecutionScheduleCreateResponse: + """Creates an execution schedule. + + :param name: Name of the execution schedule + :type name: str + :param cron: Cron defining the schedule to run the executions + :type name: str + :param configuration_id: ID of the configuration for the execution schedule + :type configuration_id: str + :param start: Start time of the execution schedule in UTC, defaults to None + :type start: datetime, optional + :param end: End time of the execution schedule in UTC e.g., defaults to None + :type end: datetime, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse` + """ + body = { + "name": name, + "cron": cron, + "configuration_id": configuration_id, + } + if start: + body['start'] = start.strftime(DATETIME_FORMAT) + if end: + body['end'] = end.strftime(DATETIME_FORMAT) + response_dict = self.rest_client.post(path='/executionSchedules', body=body, resource_group=resource_group) + + return ExecutionScheduleCreateResponse.from_dict(response_dict) + + def delete(self, execution_schedule_id: str, resource_group: str = None) -> BasicResponse: + """Deletes the execution schedule. + + :param execution_schedule_id: ID of the execution schedule to be deleted + :type execution_schedule_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'/executionSchedules/{execution_schedule_id}', + resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def get(self, execution_schedule_id: str, resource_group: str = None) -> ExecutionSchedule: + """Retrieves the execution schedule from the server. + + :param execution_schedule_id: ID of the execution schedule to be retrieved + :type execution_schedule_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved execution + :rtype: class:`ai_api_client_sdk.models.execution.Execution` + """ + execution_schedule_dict = self.rest_client.get(path=f'/executionSchedules/{execution_schedule_id}', + resource_group=resource_group) + return ExecutionSchedule.from_dict(execution_schedule_dict) + + def modify(self, execution_schedule_id: str, cron: str = None, start: datetime = None, end: datetime = None, + configurationId: str = None, status: ScheduleStatus = None, resource_group: str = None) -> BasicResponse: + """Modifies the execution schedule. + + :param execution_schedule_id: ID of the execution to be modified + :type execution_schedule_id: str + :param cron: Cron defining the schedule to run the executions, defaults to None + :type cron: str, optional + :param configurationId: ID of the configuration for the execution schedule, defaults to None + :type configurationId: str, optional + :param start: Start time of the execution schedule in UTC, defaults to None + :type start: datetime, optional + :param end: End time of the execution schedule in UTC, defaults to None + :type end: datetime, optional + :param status: pause / resume Status of the execution schedule, defaults to None + :type status: class:`ai_api_client_sdk.models.status.ScheduleStatus`, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {} + if cron: + body['cron'] = cron + if start: + body['start'] = start.strftime(DATETIME_FORMAT) + if end: + body['end'] = end.strftime(DATETIME_FORMAT) + if configurationId: + body['configurationId'] = configurationId + if status: + body['status'] = status.value + + if not body: + raise AIAPIInvalidInputException('The Request Body cannot be empty.') + + response_dict = self.rest_client.patch(path=f'/executionSchedules/{execution_schedule_id}', body=body, + resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def query(self, configuration_id: str = None, status: ScheduleStatus = None, top: int = None, + skip: int = None, resource_group: str = None) -> ExecutionScheduleQueryResponse: + """Queries the execution schedules. + + :param configuration_id: ID of the configuration, the executions should be configured by, defaults to None + :type configuration_id: str, optional + :param status: ScheduleStatus which the execution schedule should currently have + :type status: class:`ai_api_client_sdk.models.schedule_status.ScheduleStatus`, optional + :param top: Number of executions to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of executions to be skipped, from the list of the queried executions, defaults to None + :type skip: int, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse` + """ + params = self._form_query_params(configuration_id=configuration_id, + status=status.value if status else None, + top=top, skip=skip) + response_dict = self.rest_client.get(path='/executionSchedules', params=params, resource_group=resource_group) + return ExecutionScheduleQueryResponse.from_dict(response_dict) + + def count(self, configuration_id: str = None, status: ScheduleStatus = None, + resource_group: str = None) -> int: + """Counts the number of executions schedules. + + :param configuration_id: ID of the configuration, the executions should be configured by, defaults to None + :type configuration_id: str, optional + :param status: ScheduleStatus which the execution schedule should currently have, defaults to None + :type status: class:`ai_api_client_sdk.models.schedule_status.ScheduleStatus`, optional + :param resource_group: Resource group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: int + """ + params = self._form_query_params(configuration_id=configuration_id, + status=status.value if status else None) + return self.rest_client.get(path='/executionSchedules/$count', params=params, resource_group=resource_group) diff --git a/packages/base/ai_api_client_sdk/resource_clients/healthz_client.py b/packages/base/ai_api_client_sdk/resource_clients/healthz_client.py new file mode 100644 index 0000000..8521836 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/healthz_client.py @@ -0,0 +1,19 @@ +from ai_api_client_sdk.models.healthz_status import HealthzStatus +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class HealthzClient(BaseClient): + """HealthzClient is a class implemented for interacting with the healthz endpoint of the server. It implements the + base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def get(self) -> HealthzStatus: + """Retrieves the health status of the server. + + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The health status of the server + :rtype: class:`ai_api_client_sdk.models.healthz_status.HealthzStatus` + """ + healthz_status_dict = self.rest_client.get(path='/healthz') + return HealthzStatus.from_dict(healthz_status_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/meta_client.py b/packages/base/ai_api_client_sdk/resource_clients/meta_client.py new file mode 100644 index 0000000..b71f89e --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/meta_client.py @@ -0,0 +1,14 @@ +from ai_api_client_sdk.models.capabilities import Capabilities +from ai_api_client_sdk.models.version_list import VersionList +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class MetaClient(BaseClient): + + def get(self) -> Capabilities: + capabilities_dict = self.rest_client.get(path='/meta') + return Capabilities.from_dict(capabilities_dict) + + def get_versions(self) -> VersionList: + version_list_dict = self.rest_client.get(path='/meta/versions') + return VersionList.from_dict(version_list_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/metrics_client.py b/packages/base/ai_api_client_sdk/resource_clients/metrics_client.py new file mode 100644 index 0000000..bfecbc4 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/metrics_client.py @@ -0,0 +1,65 @@ +import warnings +from typing import List + +from ai_api_client_sdk.models.metrics_query_response import MetricsQueryResponse +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class MetricsClient(BaseClient): + """MetricsClient is a class implemented for interacting with the metrics related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def query(self, filter: str = None, execution_ids: List[str] = None, select: List[str] = None, resource_group: str = None) -> \ + MetricsQueryResponse: + """Queries the metrics. + + :param filter: Deprecated. Use parameter execution_ids instead. A filter expression that filters the metric + resources using execution IDs. User can only use in, eq operators in filter expression, defaults to None + :type filter: str, optional + :param execution_ids: IDs of the executions, of which the metrics should be retrieved, defaults to None + :type execution_ids: List[str], optional + :param select: Values of select can be metrics,tags,customInfo or any of the combinations of these or *. + Can be used to select(project) only the resources specified + :type select: List[str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse` + """ + params = self._form_query_params(filter=filter, execution_ids=execution_ids, select=select) + if params and 'filter' in params: # pylint: disable=unsupported-membership-test + warnings.warn('Parameter filter is deprecated. Use parameter execution_ids instead.', DeprecationWarning, stacklevel=2) + params['$filter'] = params['filter'] # pylint: disable=unsupported-assignment-operation,unsubscriptable-object + del params['filter'] # pylint: disable=unsupported-delete-operation + response_dict = self.rest_client.get(path='/metrics', params=params, resource_group=resource_group) + return MetricsQueryResponse.from_dict(response_dict) + + def delete(self, execution_id: str, resource_group: str = None) -> None: + """Deletes the metrics. + + :param execution_id: ID of the execution, of which the metrics should be deleted. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + params = self._form_query_params(execution_id=execution_id) + self.rest_client.delete(path='/metrics', params=params, resource_group=resource_group) diff --git a/packages/base/ai_api_client_sdk/resource_clients/model_client.py b/packages/base/ai_api_client_sdk/resource_clients/model_client.py new file mode 100644 index 0000000..a3e039c --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/model_client.py @@ -0,0 +1,35 @@ +from ai_api_client_sdk.models.model_query_response import ModelQueryResponse +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ModelClient(BaseClient): + """ModelClient is a class implemented for interacting with model related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + DEFAULT_SCENARIO_ID = "foundation-models" + + def query( + self, + resource_group: str = None, + ) -> ModelQueryResponse: + """Queries the models. + + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse` + """ + response_dict = self.rest_client.get( + path=f"/scenarios/{ModelClient.DEFAULT_SCENARIO_ID}/models", + resource_group=resource_group, + ) + return ModelQueryResponse.from_dict(response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/resource_groups_client.py b/packages/base/ai_api_client_sdk/resource_clients/resource_groups_client.py new file mode 100644 index 0000000..38b2e00 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/resource_groups_client.py @@ -0,0 +1,125 @@ +from typing import List + +from ai_api_client_sdk.models.base_models import BasicResponse +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.resource_group import ResourceGroup +from ai_api_client_sdk.models.resource_group_query_response import ResourceGroupQueryResponse +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ResourceGroupsClient(BaseClient): + """ResourceGroupsClient is a class implemented for interacting with the resource groups endpoints of the server. + It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/admin/resourceGroups' + + def create(self, resource_group_id: str, labels: List[Label] = None) -> ResourceGroup: + """Creates resource group for a given tenant. + + :param resource_group_id: the id of the resource group and the length must be between 3 and 10 characters. + :type resource_group_id: str + :param labels: key-value pairs of the labels that will be added to the resource group. + :type labels: List[Label] + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup` + """ + body = { + 'resourceGroupId': resource_group_id, + } + + if labels: + body['labels'] = [l.to_dict() for l in labels] + + response_dict = self.rest_client.post(path=f'{self.__PATH}', body=body) + return ResourceGroup.from_dict(response_dict) + + def delete(self, resource_group_id: str) -> BasicResponse: + """Deletes the resource group. + + :param resource_group_id: the id of the resource group to be deleted + :type resource_group_id: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{resource_group_id}') + return BasicResponse.from_dict(response_dict) + + def get(self, resource_group_id: str) -> ResourceGroup: + """Gets a resource group of a given tenant. + + :param resource_group_id: the id of the resource group to be retrieved + :type resource_group_id: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the resource group from the server + :rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{resource_group_id}') + return ResourceGroup.from_dict(response_dict) + + def modify(self, resource_group_id: str, labels: List[Label]) -> None: + """Modifies a resource group. + + :param resource_group_id: the id of the resource group + :type resource_group_id: str + :param labels: key-value pairs of the labels that will be added to the resource group. + :type labels: List[Label] + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + """ + + body = { + 'labels': [l.to_dict() for l in labels] + } + + self.rest_client.patch(path=f'{self.__PATH}/{resource_group_id}', body=body) + + def query(self, search: str = None, search_case_insensitive: bool = None) -> ResourceGroupQueryResponse: + """Get all resource groups. + + :param search: Generic search term to be looked for in various attributes of resource groups, defaults to None + :type search: str, optional + :param search_case_insensitive: Indicates whether the search should be case insensitive + :type search_case_insensitive: bool, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of resource groups for a given tenant. + :rtype: class:`ai_core_sdk.models.resource_group_query_response.ResourceGroupQueryResponse` + """ + params = self._form_query_params(search=search, search_case_insensitive=search_case_insensitive) + response_dict = self.rest_client.get(path=f'{self.__PATH}', params=params) + return ResourceGroupQueryResponse.from_dict(response_dict) diff --git a/packages/base/ai_api_client_sdk/resource_clients/scenario_client.py b/packages/base/ai_api_client_sdk/resource_clients/scenario_client.py new file mode 100644 index 0000000..c93dd36 --- /dev/null +++ b/packages/base/ai_api_client_sdk/resource_clients/scenario_client.py @@ -0,0 +1,102 @@ +from typing import List + +from ai_api_client_sdk.helpers.llm_helper import filter_for_llm_scenarios +from ai_api_client_sdk.models.scenario import Scenario +from ai_api_client_sdk.models.scenario_query_response import ScenarioQueryResponse +from ai_api_client_sdk.models.version_query_response import VersionQueryResponse +from ai_api_client_sdk.resource_clients.base_client import BaseClient + + +class ScenarioClient(BaseClient): + """ScenarioClient is a class implemented for interacting with the scenario related endpoints of the server. It + implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + def get(self, scenario_id: str, resource_group: str = None) -> Scenario: + """Retrieves the scenario from the server. + + :param scenario_id: ID of the scenario to be retrieved + :type scenario_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved scenario + :rtype: class:`ai_api_client_sdk.models.scenario.Scenario` + """ + scenario_dict = self.rest_client.get(path=f'/scenarios/{scenario_id}', resource_group=resource_group) + return Scenario.from_dict(scenario_dict) + + def query(self, only_llm_scenarios: bool = False, resource_group: str = None) -> ScenarioQueryResponse: + """Queries the scenarios. + + :param only_llm_scenarios: indicates whether to query for LLM scenarios only, defaults to False + :type only_llm_scenarios: bool, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` + """ + response_dict = self.rest_client.get(path='/scenarios', resource_group=resource_group) + if only_llm_scenarios: + response_dict = filter_for_llm_scenarios(response_dict) + return ScenarioQueryResponse.from_dict(response_dict) + + def query_llm_scenarios(self, resource_group: str = None) -> ScenarioQueryResponse: + """Queries for the LLM scenarios. + + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIRequestException` if an unexpected exception occurs while + trying to send a request to the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` + """ + return self.query(only_llm_scenarios=True) + + def query_versions(self, scenario_id: str, label_selector: List[str] = None, resource_group: str = None) -> \ + VersionQueryResponse: + """Queries the versions. + + :param scenario_id: ID of the scenario, the versions should belong to + :type scenario_id: str + :param label_selector: list of the label selector strings in the form of "key=value" or "key!=value", to filter + the scenarios with respect to their labels, defaults to None + :type label_selector: List[str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` + """ + params = self._form_query_params(label_selector=label_selector) + response_dict = self.rest_client.get(path=f'/scenarios/{scenario_id}/versions', params=params, + resource_group=resource_group) + return VersionQueryResponse.from_dict(response_dict) diff --git a/packages/base/docs/CHANGELOG.md b/packages/base/docs/CHANGELOG.md new file mode 100644 index 0000000..ed6e7e4 --- /dev/null +++ b/packages/base/docs/CHANGELOG.md @@ -0,0 +1,1061 @@ +# [3.4.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.3.0...v3.4.0) (2026-03-11) + + +### Features + +* **rest_client:** add convert_params_to_camel_case parameter ([ad57be5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ad57be582c74a91b552ed8c2473f0bd36641935d)) + +# [3.3.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.2.3...v3.3.0) (2026-03-05) + + +### Features + +* **authenticator:** add retry logic to get_token ([9727dd9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9727dd9806b816527d6f978d30df9b69ed1f368e)) + +## [3.2.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.2.2...v3.2.3) (2026-01-22) + + +### Bug Fixes + +* **deps:** update dockerio.int.repositories.cloud.sap/python docker tag to v3.14 ([f8d457f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f8d457fb4a0a2de3f35d84a7bbe46703febac927)) + +## [3.2.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.2.1...v3.2.2) (2026-01-19) + + +### Bug Fixes + +* **requirements:** add missing blank line at eof ([468541f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/468541f3b692ffd4b6f813bb044288dbc279c5f8)) +* **requirements:** version update ([73ce1d6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/73ce1d61699a0d40eba82109bc3027bd2a2f3c59)) + +## [3.2.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.2.0...v3.2.1) (2025-12-02) + + +### Bug Fixes + +* **deps:** update dependency pytest-cov to v7 ([12488ec](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/12488ecc79b7846908fa6c93fab5f6e092763053)) + +# [3.2.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.9...v3.2.0) (2025-11-24) + + +### Bug Fixes + +* **test:** adjust tests ([a16c82d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a16c82d1ca329fc688c2936d3f7be2afb3a77c56)) +* **test:** backup environment variable in tests ([c8f20e3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c8f20e36299b5e1b6ee2cc287bbf8084dcd5235a)) +* **test:** fix test args ([346917c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/346917c577f66d4525709441d0beb01d2fb57a44)) + + +### Features + +* **client:** add AI_CLIENT_TYPE environment var to set client type ([b301f3a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b301f3af7c4d78a2defd856b3445c302a685ddf8)) + +## [3.1.9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.8...v3.1.9) (2025-11-20) + + +### Bug Fixes + +* **skip auth:** skip auth in client class ([2c537c6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2c537c68ef32da92c7516f270a7284eae9799f83)) + +## [3.1.8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.7...v3.1.8) (2025-11-19) + + +### Bug Fixes + +* **deps:** update dependency pytest to v9 ([a4f8388](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a4f8388af6d61b293e37fd1b2d8cff707e49af66)) + +## [3.1.7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.6...v3.1.7) (2025-10-15) + + +### Bug Fixes + +* **sonar:** replace with vault token ([44477d6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/44477d6dd6c17947a012e367d2a1d0bbdd199437)) +* **tests:** additional test fixes ([fbd0ddb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/fbd0ddb18054ceeac4a73e533e896ae995904021)) +* **tests:** correct resource group tests ([eb8c1cb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/eb8c1cb9628e23e59f116be215b4a7f0dbb0cab6)) + +## [3.1.6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.5...v3.1.6) (2025-08-06) + + +### Bug Fixes + +* don't change to camel case when required by API ([#211](https://github.wdf.sap.corp/AI/ai-api-client-sdk/issues/211)) ([a5a3193](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a5a319329253b59932f74103c1a55e3ae9e6deab)) + +## [3.1.5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.4...v3.1.5) (2025-08-05) + + +### Bug Fixes + +* **setup:** switch to python 3.13 ([45fb9a2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/45fb9a2f31475478cbb267e6dfaeae2a0bccac6c)) + +## [3.1.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.3...v3.1.4) (2025-08-05) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.23.5 ([a96d45a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a96d45a40e9c0daa3ad688a18a87f4d282a95803)) +* **test:** use intwdf cluster ([73a089b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/73a089bd969044747d930344a83f5900a4f6f66b)) + +## [3.1.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.2...v3.1.3) (2025-06-24) + + +### Bug Fixes + +* **auth:** skip auth header with the env variable is set ([f76b556](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f76b556d5a1d4b197614bba5d37ee859b62737b5)) + +## [3.1.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.1...v3.1.2) (2025-06-05) + + +### Bug Fixes + +* **dummy:** dummy commit ([4045a54](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4045a54e043235063fb57a00add31e7ce487d378)) +* **dummy:** Update README.md ([938e78c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/938e78c3e497aafa933f68a33282c0e6c9bc4e2a)) + +## [3.1.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.1.0...v3.1.1) (2025-06-03) + + +### Bug Fixes + +* **setup:** fix intended audience spaces error ([cb0053d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/cb0053d45ace9b60de48cdbd80e0b381cdc21fda)) + +# [3.1.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v3.0.0...v3.1.0) (2025-05-28) + + +### Bug Fixes + +* **chore:** adhere all python files to PEP8 style guide for imports ([98f4a2f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/98f4a2f5eeac0b1ed7bc82c597d0a33a473a78b9)) +* **chore:** fix sonar issues ([397eb78](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/397eb78d17750f04cc1b3fb8943ff40e6d1766e7)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.41 ([5079af9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5079af91a12ced2d9a45715098e61e10c8e55874)) + + +### Features + +* **model-discovery:** add new fields to model and model_version ([734ce0e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/734ce0e142531ad34f7c104da75089435f6437a8)) + +# [3.0.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.6.1...v3.0.0) (2025-05-14) + + +* flag for pypi release ([7a52e29](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7a52e2989722635016f76d92f6821b273695352b)) + + +### Features + +* **package-name:** rebranding Sapphire ([039d509](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/039d5094b4abfe0a83190715fa4425aae8faa766)) +* **package-name:** rebranding-AIWDF2526 ([35638e8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/35638e8cb77b07f737583e7b96c7881b88662127)) +* **package-name:** rebranding-AIWDF2526 ([8b8b456](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/8b8b4564b70d4eac71f4a9eb74cefe26b3e45729)) + + +### BREAKING CHANGES + +* change distribution name to sap-ai-sdk-base +* **package-name:** change distribution name to sap-ai-sdk-base +* **package-name:** change distribution name + +## [2.6.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.6.0...v2.6.1) (2025-05-14) + + +### Bug Fixes + +* **ci:** dummy commit to fix ci ([e80cbd7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e80cbd7098defdeacdfe9e89f731cf9639b8ac73)) + +# [2.6.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.5.0...v2.6.0) (2025-05-13) + + +### Features + +* **package-name:** rebranding-AIWDF2526 ([3ccde4d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3ccde4d12b24056979f5d3f7fe66f53db010bbe1)) + +# [2.5.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.11...v2.5.0) (2025-04-29) + + +### Features + +* **deprecation:** rebranding-AIWDF2525 ([7a5e45e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7a5e45ee4e86f1d3e669c30c06bedf1c45cea6fd)) + +## [2.4.11](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.10...v2.4.11) (2025-04-28) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.22 ([1865688](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/18656888cb73da186062f2c026d19b24a371d535)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.29 ([0ee5622](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0ee5622e08de4ff64242c3039a73e04faa3033e7)) + +## [2.4.10](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.9...v2.4.10) (2025-03-24) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.18 ([c9bfb9a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c9bfb9adc81e9c06e349cdb1c9b4288809f200ee)) + +## [2.4.9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.8...v2.4.9) (2025-03-14) + + +### Bug Fixes + +* **blackduck:** add blackduck ctp scan AIWDF-2444 ([88806e0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/88806e05da6f66df5587b6c03156b04e51e257ca)) +* **client type header:** give option to set client header ([e5d9de2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e5d9de2347687adc43a996e4be618b84362d4bd2)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.17 ([63531dc](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/63531dcab44d4c0fb02f9f17ac90b9431f56bb8e)) +* **tests:** fix resource group tests ([13f4f3f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/13f4f3fc792ef09ac180adce3bf609ea3956dbf0)) + +## [2.4.8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.7...v2.4.8) (2025-02-17) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.9 ([f8e3ff8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f8e3ff88ce05f3f306bf4675aab7417bc3ae4fa4)) + +## [2.4.7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.6...v2.4.7) (2025-01-28) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.5 ([74dfe3f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/74dfe3f6c2e7869572fcec43220ea5a49b2c6ebd)) + +## [2.4.6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.5...v2.4.6) (2025-01-28) + + +### Reverts + +* Revert "fix rest client when using bytes response" ([4c8f101](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4c8f1017232bdd69069b993c78ae172bf6c6d651)) + +## [2.4.5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.4...v2.4.5) (2025-01-23) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.14.9 ([9a7aef4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9a7aef456f3993b99beab31a0fe2f49bcb96b6bb)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.2 ([7ac3e2e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7ac3e2e4990cb2b42123d1eb10d1cb29f4e40521)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.4 ([49fb649](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/49fb649c7ff92b3c92dc411836a695e55ac73765)) + +## [2.4.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.3...v2.4.4) (2024-12-10) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.14.4 ([#166](https://github.wdf.sap.corp/AI/ai-api-client-sdk/issues/166)) ([5b773cc](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5b773cc9babd9b8c1505a0e5a1ce47119957d115)) + +## [2.4.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.2...v2.4.3) (2024-12-10) + + +### Bug Fixes + +* **content-type:** fix content-type for bulk modify ([a3745d0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a3745d0f7659d8bac83812fd96cbcb3d1d4e3023)) +* **deps:** update dependency pytest-cov to v6 ([9288eb0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9288eb0c7114b8f630ac3a194747191adc8a3133)) + +## [2.4.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.1...v2.4.2) (2024-11-23) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.32 ([#162](https://github.wdf.sap.corp/AI/ai-api-client-sdk/issues/162)) ([1e1e008](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1e1e0087ec92831493fd5b3e14e573dee517df82)) + +## [2.4.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.4.0...v2.4.1) (2024-11-05) + + +### Bug Fixes + +* **cicd:** test collect policy results step ([9f13be3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9f13be3d722ced97fc496d90bf487dd986622d4a)) + +# [2.4.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.3.1...v2.4.0) (2024-10-24) + + +### Features + +* **rest-client:** Add capabilities to debug log api calls in rest_client.py ([b792f1c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b792f1c65318da9d936cc7ba2a9882694bce7c37)) + +## [2.3.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.3.0...v2.3.1) (2024-10-21) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.28 ([9603cd5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9603cd5971a1da32304f3325d343cb3227718f27)) + +# [2.3.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.5...v2.3.0) (2024-10-18) + + +### Bug Fixes + +* **model-discovery:** return retirement_date as a datetime object ([af5fa1b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/af5fa1ba172da753ce95097730fa4b73e730bd51)) + + +### Features + +* **model-discovery:** add deprecated and retirementDate fields ([dc82766](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/dc827662c615d2a1ce94093a7a666bd0a6384319)) + +## [2.2.5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.4...v2.2.5) (2024-10-14) + + +### Bug Fixes + +* **checkmarx:** switch to checkmarx one ([9219f79](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9219f7924f1c4d79a6b845ad711e7aaea695ef71)) + +## [2.2.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.3...v2.2.4) (2024-10-11) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.22 ([5cc00d3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5cc00d3870a70d524b479107f5708aa100e40c58)) + +## [2.2.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.2...v2.2.3) (2024-10-10) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.2 ([384ea59](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/384ea599528013f31e3f6fe16e8fc1e43b728b8d)) + +## [2.2.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.1...v2.2.2) (2024-09-16) + + +### Bug Fixes + +* **version:** fix version.txt ([4fb2362](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4fb2362d2b44d4a7867938b7cbf3d270f243effa)) +* **x509:** fix x509 authorization ([1c59b31](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1c59b312e4d3f2d82fa9ce1051eeb7f78a0b3392)) + +## [2.2.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.2.0...v2.2.1) (2024-08-05) + + +### Bug Fixes + +* **blackduck:** docker image fixes for java ([ebdaaf9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ebdaaf98b78684d8b98631ad60fb06f68ef95c14)) +* **blackduck:** enable signature scan in blackduck ([ff44dc5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ff44dc51e97ca33b0ef2c071d38113dcc4d5dfd8)) + +# [2.2.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.5...v2.2.0) (2024-07-15) + + +### Features + +* **query-models:** Introduces ability to query available models. ([e0fa19a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e0fa19a9b9d12f7215d3504caef12223692a6b04)) + +## [2.1.5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.4...v2.1.5) (2024-07-15) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.5.15 ([5e20701](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5e2070102522e23ae0975aa94a04773f308d168a)) + +## [2.1.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.3...v2.1.4) (2024-06-25) + + +### Bug Fixes + +* **renovate:** improves renovate config ([3fa2593](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3fa25933e049664611cb5023718eb2395bf34b0d)) + +## [2.1.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.2...v2.1.3) (2024-06-19) + + +### Bug Fixes + +* **blackduck:** file upload for blackduck ([4425277](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/442527708097bb70e3cf9077fcf214f0ae8791cf)) + +## [2.1.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.1...v2.1.2) (2024-06-11) + + +### Bug Fixes + +* **setup:** fixed setup for generating correct PKG-INFO ([f3819fc](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f3819fcdcea456943ba3cfd8a26ca958d0abc8ec)) + +## [2.1.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.1.0...v2.1.1) (2024-06-05) + + +### Bug Fixes + +* **blackduck:** add blackduck to cumulus ([1434e41](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1434e4165ecde9fe00fd5e8f40669c3bbe9f5b56)) + +# [2.1.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.0.4...v2.1.0) (2024-03-26) + + +### Bug Fixes + +* **test:** dummy commit ([bdfe3bd](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bdfe3bd0d2fec293b0d8cb544a446ea6dab12ff8)) + + +### Features + +* **x509:** enable x509 ([1623d7e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1623d7e4a2d495f9af7293f09430c717400c645a)) + +## [2.0.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.0.3...v2.0.4) (2024-01-25) + + +### Bug Fixes + +* **datetime:** include timezone info ([1f1f5d1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1f1f5d1fa2a154752738b520d9c11220142e93fb)) +* **test:** fix response obj ([a79c3cb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a79c3cbe1fa3c3b307625b70cb5d25ac5f296709)) + +## [2.0.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.0.2...v2.0.3) (2024-01-10) + + +### Bug Fixes + +* **checkmarx:** set correct preset ([d51d684](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d51d6847fbfa3547f229c938891c24d81ab6b7c6)) + +## [2.0.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.0.1...v2.0.2) (2023-12-13) + + +### Bug Fixes + +* **versions:** update dependencies ([3672b7a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3672b7a9e029afa3ab294a6f962a71eb6434d0dc)) + +## [2.0.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v2.0.0...v2.0.1) (2023-12-07) + + +### Bug Fixes + +* **nose:** remove nosetests dependency ([e8143ab](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e8143ab85be928b74ffcb813bb589eb0c0dc9b4c)) + +# [2.0.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.30.0...v2.0.0) (2023-12-05) + + +### deprec + +* **error-handling:** Deprecate AIAPIRequestException ([2169b77](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2169b77c4df36f4e3e95eb594c5ec99046f3d8c6)) + + +### BREAKING CHANGES + +* **error-handling:** The AIAPIRequestException has been removed. Instead of this exception, the more specific, originally raised exception is being thrown. + +# [1.30.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.29.0...v1.30.0) (2023-11-30) + + +### Bug Fixes + +* **llm_check:** changes based on review ([c1ddad8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c1ddad8193bc60d7b519bd3a38df291fd27422f6)) +* **llm_check:** check if executable and deployment belongs to llm scenario ([d5e5baf](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d5e5baf51c51c1a992c5f6233ce0ec3764f04ccb)) +* **llm_check:** implement review feedback ([555cdec](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/555cdec8a92bba3572be4bdaa4a88a5e7dd1990f)) +* **llm_check:** move partial functions outside ([35eb3e3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/35eb3e3f973440e19153aa1407564914737430d0)) + + +### Features + +* **llm:** add query for llm scenarios ([8cba68e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/8cba68ea8bfa1f799009e0d7e6650ad0c93e479c)) + +# [1.29.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.28.2...v1.29.0) (2023-11-14) + + +### Bug Fixes + +* **caching:** add buffer of 60 minutes and add new unit tests ([9e64984](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9e6498479267c359b5fbbc8a23e87f898dd1151a)) +* **decode:** disable verify exp ([3da2543](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3da2543be8d0fd7038b9c1c8cfd4b7e2e87f398c)) +* **error:** e2e error ([4c2d5ab](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4c2d5ab519ea558ca334028926aceff8777c969d)) +* **sonar:** implemented pylint recommendations ([444e7f4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/444e7f4d93da7a76266d8ed5affffe009837797b)) +* **tests:** fix tests jwt.exceptions.InvalidAudienceError: Invalid audience ([44dbccf](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/44dbccfeffb3d38503d5443ad828fd57dbe606b1)) + + +### Features + +* **caching:** add caching to authentication ([35d0d85](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/35d0d8561a161aa62aa4626affb783318a95d817)) +* **caching:** refactoring ([d9f07d1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d9f07d154b8b2aca134a3d4e89d21fcc599db75b)) +* **caching:** use the expires_in property in http response to calculate the expiration date ([1960d9d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1960d9dc2dc9e72220f01c9529abaedd11b7d8b3)) + +## [1.28.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.28.1...v1.28.2) (2023-11-07) + + +### Bug Fixes + +* **deployments:** add missing details information to deployment ([dee3b80](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/dee3b80b2a62f626ee8ccf72f930043cebcae897)) +* **meta:** add test ([5c58315](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5c5831593867becda84ec6d89d87dcfc6e24a6a2)) + +## [1.28.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.28.0...v1.28.1) (2023-10-02) + + +### Bug Fixes + +* **error-handling:** Fix error handling of rest-client ([ddf049b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ddf049b88c0bc3aa396772dc1054ca88efae2ae5)) +* **error-handling:** Fix error handling of rest-client ([575b29a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/575b29a72f74d04133637646aad7861dfaf075cc)) +* **error-handling:** Fix error handling of rest-client ([f1d5986](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f1d59860d50398aa157fc6ce5065e708de3330fa)) + +# [1.28.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.27.0...v1.28.0) (2023-05-31) + + +### Bug Fixes + +* review comments ([db4e7fd](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/db4e7fd50b8b71cf13512e52902a32888c9999d8)) +* review comments ([f4a9c08](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f4a9c0887b9a098aec04a7b15a417428ea17941a)) +* review comments ([b5bd73b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b5bd73b9f38d7654965cd7ec0526b59055033043)) +* review comments ([a7bf2d4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a7bf2d4b0333cf46e2c4b2088b7635bba7f07074)) +* review comments ([0f633d8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0f633d8d282d70c7556911bb569bb827b44d6546)) +* review comments ([cd03ec5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/cd03ec59053eb38de2ea2d8fed51d2eb844edf26)) +* unit tests ([4084b1e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4084b1eee986da49a8bff91b6eeb8c7f7286c401)) + + +### Features + +* add matadata param to artifact and parameters ([5a98bcb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5a98bcbae679da41dc022d1a65037e32458958a5)) + +# [1.27.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.26.3...v1.27.0) (2023-05-09) + + +### Bug Fixes + +* add status details ([57ecef4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/57ecef4ed205ce6511a556ea2e849b639d5f847c)) +* add test for status-details ([f0b2355](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f0b23555b380dc1c0cfd559e96aee5870019000d)) +* add union for rtype ([7505062](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7505062cef4ab9e7b24d8a4e940d4dbf4a0cb462)) +* apply feedback ([9a1ccfc](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9a1ccfc0466117aefe52464fd828ebd3c4e7e781)) +* apply feedback ([3ad9414](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3ad9414d315708d11c9be2d49ef76bdfcf02b095)) +* clean code ([850c5f2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/850c5f20546a1a0e241a63601d57b455d5acacb0)) +* rm the convenience func ([c526194](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c526194b88373645c17b6bdd97f823a65fc55631)) + + +### Features + +* **get-status:** add get status for execution and deployment ([04b7455](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/04b7455d2b567b6bc39772b656ec33f95d60b1a0)) + +## [1.26.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.26.2...v1.26.3) (2023-04-28) + + +### Bug Fixes + +* **meta:** add bulk updates and execution schedules to meta ([8b13533](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/8b1353391d8cbb097e1dfb8f68bd3ddb605a6f71)) + +## [1.26.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.26.1...v1.26.2) (2023-03-20) + + +### Bug Fixes + +* **release:** trigger release ([5a254ac](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5a254acb051e86b6a017630d55688d94a21af28d)) + +## [1.26.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.26.0...v1.26.1) (2023-02-17) + + +### Bug Fixes + +* **ppms:** disable ppms compliance check ([1c31d91](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1c31d91a2b30f656b8b06d8c76e99d78208d18b6)) + +# [1.26.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.25.0...v1.26.0) (2023-02-06) + + +### Features + +* **rest-client:** Add functionality to add own headers to requests for rest_client.py ([f887739](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f887739b678db9aef772c28fded8aef7a209d43c)) + +# [1.25.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.24.2...v1.25.0) (2023-01-18) + + +### Bug Fixes + +* **bulk_modify:** address code reviews ([88daa67](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/88daa670c6939cbcd86ea979b62c84658114f86f)) +* **model:** fix BasicModifyRequest ([5c89692](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5c89692e0efa1486810a34f6f1791c5b8a63511e)) +* **sdk:** added unit test ([aa3c0bb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/aa3c0bbc0539c29db258a00082a87fe79a6c803e)) +* **sdk:** alignment change ([c502566](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c502566ddd9178921d33a5aecadeed07f0d1f863)) +* **sdk:** alignment changes ([c4284d2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c4284d2f65b1099c5af916e1dd01e831a994b790)) +* **sdk:** bulk modify deployments integration test ([d1c05a3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d1c05a311d93450f2c5a1a0efd5d117809709642)) +* **sdk:** bulk modify executions, deployments integration test ([b064620](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b064620b4ef90c80b01c4d485aabc48ec42c80ed)) +* **sdk:** bulk_modify not implemented ([e6cab24](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e6cab24b4dc66b9eb38c2b788da7a20452af5889)) +* **sdk:** code review changes ([d09839a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d09839a3c4024961d5e6554b5496a3d8c67f4fc8)) +* **sdk:** code review changes ([e7c3654](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e7c3654e32da28a69848e09446628e08213156b2)) +* **sdk:** code review changes ([9c6648b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9c6648b7eb657bfb10f43d96d4ae19aa48ac0634)) +* **sdk:** code review suggestions ([e9644a7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e9644a7393ccfb40fc42e39f93770abea15167d8)) +* **sdk:** comments correction ([460b785](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/460b7856e062bdb359153bf05f331bfad19824d6)) +* **sdk:** integration tests ([d3e89cc](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d3e89cc68be44a15d731677ee95c03fab0d61604)) +* **sdk:** remove unwanted code ([2036282](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/203628222a0575db276f1d11daaa5d30621d3bc5)) +* **sdk:** revert base model changes ([b23e279](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b23e279c757ebef2fcb465bf1fb44205b8188e07)) +* **sdk:** stop and delete multiple executions ([a9f4674](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a9f46745fe444c0739bb0f124eab07a412c90194)) +* **test:** fix execution e2e test ([fd40ade](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/fd40ade7105dc56754e65f51001b421997b7534d)) +* **tests:** add tests to increase coverage ([de9a00d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/de9a00deb707e6ff13a8a574c1507dbbf08492b5)) + + +### Features + +* batch update of executions ([1a3e511](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1a3e511142d8a6bd4d4de62208b25248386b0bf2)) + +## [1.24.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.24.1...v1.24.2) (2023-01-17) + + +### Bug Fixes + +* **packaging:** source package missing .md file ([686aec6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/686aec63bbd67a5e77d6874768baf68ba268697b)) + +## [1.24.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.24.0...v1.24.1) (2022-11-16) + + +### Bug Fixes + +* **deployment:** fix last operation ([5c51a2c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5c51a2cdce201a1729bebf09469a5cf83af6f00c)) + +# [1.24.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.23.1...v1.24.0) (2022-09-22) + + +### Bug Fixes + +* **api:** add ttl to deployment response ([83f1b3b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/83f1b3be59bea05828604624f629defce57561a8)) +* **api:** apply feedback ([2c61a2d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2c61a2d5a90f3d336bbf8839c778db8f708f1da4)) +* **api:** apply feedback ([a893654](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a893654befd75198c8290949d010e1526562b478)) + + +### Features + +* **api:** add timeToLive for deployments ([26d6895](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/26d6895defe10a5cc0923a7453d5b23b6d2deaf3)) +* **api:** add ttl to deployment ([bef032e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bef032eb5a35513e89accc22b7f530f0c9484c8c)) + +## [1.23.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.23.0...v1.23.1) (2022-09-09) + + +### Bug Fixes + +* **query-responses:** Fix string representation of query responses ([a2f86c6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a2f86c6cafa01b1499971b17ddeb1320a3ff8e80)) +* **query-responses:** Fix string representation of query responses ([50f5f98](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/50f5f98a1e150ef1948c6633e0e7a10a657e4ecc)) +* **query-responses:** Fix string representation of query responses ([0de21e1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0de21e11b9907c596073b4baba3fa359a51ac08b)) +* **query-responses:** Fix string representation of query responses ([ebbae65](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ebbae653dc1e3c4101f330da7a014527243246dd)) + +# [1.23.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.22.1...v1.23.0) (2022-09-01) + + +### Features + +* **search:** enable case insensitive search ([4932845](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/49328456e56b7f303a47a7059c5a41e0e2b1a45e)) +* **search:** Implemented feedback ([d83b4a5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d83b4a5da9951792dfa656b8a01cd609e83eceb5)) + +## [1.22.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.22.0...v1.22.1) (2022-08-18) + + +### Bug Fixes + +* **cumulus:** mapping of cumulus pipelines ([527efb6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/527efb694fa28e45b2131a2827069af1f3730b33)) + +# [1.22.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.21.0...v1.22.0) (2022-08-12) + + +### Features + +* **enactments:** Added feedback for: Added support for optional field "statusMessage" for Execution/Deployment ([38b055b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/38b055b65f8918ac07dc94b0090bd15132c8163c)) +* **enactments:** Added support for optional field "statusMessage" for Execution/Deployment ([a1e3ab7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a1e3ab7946d52c834727018aaf94dbe1a389ca01)) + +# [1.21.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.20.0...v1.21.0) (2022-08-03) + + +### Bug Fixes + +* **doc:** fix ExtensionsDataset doc ([0aebf95](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0aebf95fd7a7996f733a82a8a6b3473345eb6f99)) +* **models:** add str representations ([796269c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/796269c6e3dd0d44b67531f66a3fc34d8ec8a202)) +* **test:** fix meta client test ([39cf1c5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/39cf1c523447f66e5da29942bcf16551d975ec36)) +* **test:** fix meta e2e test ([2154c49](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2154c493a973afb4f6e279b506e99076617e7dbf)) + + +### Features + +* **meta:** add meta versions endpoint ([d99ebc8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d99ebc8d4b550d280c981f49d60686564faa7cd2)) +* **meta-api:** implement meta-api client ([f84d1e0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f84d1e0205ac26b553a379fab41fe7c7625e1d6b)) + +# [1.20.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.19.1...v1.20.0) (2022-07-28) + + +### Features + +* **objects:** Added further tests for better string representation of objects ([9e57af9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9e57af9c28b12ec38e0d0c0240347a03e1cfcde6)) +* **objects:** Better string representation of objects ([b5fd47a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b5fd47a64602ad29ba6befbcba4e0a5f2f1e8959)) +* **objects:** Implemented feedback for better string representation of objects ([b2bac65](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b2bac65300b5fc813733c0aaa32b5bd1f23fe6a6)) +* **objects:** Implemented feedback for better string representation of objects ([86abc2b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/86abc2bbcff35f769fcf127693385d0d0e478662)) +* **objects:** Implemented feedback for better string representation of objects ([e80b267](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e80b26737d19fcde120a272ea350c95e93d5e66e)) +* **objects:** Implemented feedback for better string representation of objects ([66d76be](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/66d76be558ff8ca0fb3d2005648932f573ab7cd4)) +* **objects:** Implemented feedback for better string representation of objects ([626e51c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/626e51cda5b7e897e777d5d08d1c03b47744b88a)) +* **objects:** Implemented feedback for better string representation of objects ([62ef71d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/62ef71d6fe5749ff7974220a9734bb8b58aa4eab)) +* **objects:** Implemented feedback for better string representation of objects ([8656436](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/865643618b34599ea88a36e91595ca8ef0bdb1e6)) +* **objects:** Implemented feedback for better string representation of objects ([f4461db](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f4461db57297af0c26ccf9a9c1cad0b03f7f4d90)) + +## [1.19.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.19.0...v1.19.1) (2022-07-06) + + +### Bug Fixes + +* **checkmarx:** add checkmarx to cumulus ([672e41e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/672e41e109cf4d068775355b86eba6fbca3d7f18)) + +# [1.19.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.18.0...v1.19.0) (2022-07-06) + + +### Features + +* **exception:** add debug info to AIAPIServerException ([62a5e12](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/62a5e12b53bd8694bfe46034d5e052abbc2ad614)) + +# [1.18.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.7...v1.18.0) (2022-07-04) + + +### Features + +* **restclient:** Added timeout and retries ([9eb75be](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9eb75be15037515a3ffd45d21800108f6f7a4138)) +* **restclient:** Added timeout and retries ([bbe6eca](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bbe6eca420d92385cbf74a7a09c3146c093e62b4)) +* **restclient:** Implemented feedback ([8e37144](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/8e3714495ee3a09edc2392ee03ddd6d036db7f41)) +* **restclient:** Implemented feedback ([dafa9db](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/dafa9db7d535611b16c62254869188d606235431)) +* **tests:** Adapted tests to updated restclient ([0aa4875](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0aa48753a5b26bd1a4ba788191461c9174db015f)) + +## [1.17.7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.6...v1.17.7) (2022-06-29) + + +### Bug Fixes + +* **cumulus:** extend with more upload stuff ([5c90059](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5c90059031a8988b5ebc54fddcd04ac0f21b97bc)) +* **cumulus:** extend with sonarqube ([506b72a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/506b72ac50da623874f95eef6a0ece63f8352046)) +* add protecode scans to cumulus ([4314320](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/43143203be342120db3ff474f11f92fc7bb942aa)) +* align pyhumps dependency with ai-api-sdk ([3bbdc40](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3bbdc401bd5116efd71847cb4a3232188204a8ef)) + +## [1.17.6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.5...v1.17.6) (2022-06-29) + + +### Bug Fixes + +* **chore:** change gs image url ([aa99cda](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/aa99cdabb7b52814e558bfab71f84f1e0b504202)) +* **chore:** print rg name ([9a4857e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9a4857e9470d0907a8f890a9a83bcde07c93d12d)) + +## [1.17.5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.4...v1.17.5) (2022-05-23) + + +### Bug Fixes + +* **docu:** Fix keyword definition for pypi ([d114bb2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d114bb2390c4acf8316f19e1facf7fa54d433ece)) + +## [1.17.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.3...v1.17.4) (2022-05-17) + + +### Bug Fixes + +* **docu:** Fix link to sap.com ([3e195e6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3e195e63259b65aa91a705d90ad275938c19d06a)) + +## [1.17.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.2...v1.17.3) (2022-05-10) + + +### Bug Fixes + +* **documentation:** Move documentation to pypi ([e5c199d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e5c199d101d8b7e8d0fb7abb54aa92e33a8df85e)) + +## [1.17.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.1...v1.17.2) (2022-05-03) + + +### Bug Fixes + +* **deployment:** add missing properties to deployment ([a051b64](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a051b6457c0482dd7c84f0331f389c45b73d77fe)) + +## [1.17.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.17.0...v1.17.1) (2022-04-21) + + +### Bug Fixes + +* added select optional flag ([512913c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/512913c729620faeb4523ca5fb7d34d799ad6dfa)) +* addressed review comment ([13e88e5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/13e88e5bd493cc89afac4f3acea9cbe6bd1f75b0)) +* addressed review comments ([000bf1f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/000bf1fe340fae0fad3bd2e65bd46f0eb7a71d00)) +* dummy commit ([ad1343d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ad1343da5a0aae9088e02cd39378ee3459df7394)) +* fixed custom info issue ([e327b36](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e327b3631da0d372f03ddba7f90adfcd0c59465f)) +* fixed docstring for select ([48932b7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/48932b79420c74e60f2f1fbdd6db0fa2f3da250c)) +* fixed integration test case ([afab04d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/afab04dae5c73b9ba8b5d78edfb52664488f84e6)) +* fixed integration test case ([bb495c2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bb495c247a67aee04c257c74efaf167e9949d406)) +* fixed integration test case issue ([8812a09](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/8812a09b63a20c63e3aafc7c85ebbc0541a8e351)) +* fixed integration test case issue ([cda58aa](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/cda58aac41a348880bbd9c4aea6dafbc9f3be43e)) +* fixed select param issue ([def4d4e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/def4d4edbfd1bdfbfc31379c78af43a4d99516b8)) +* fixed test case ([69efeca](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/69efeca64285512b0934891495a613f7bea52a8d)) +* fixed unit test case ([5b9302c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5b9302c1b0fdcdc41d7e3ed2cbb689ba290f8d01)) +* fixed unit test case ([1631e57](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1631e575caf2b12d2eec47b2eb3e9a4fafa9accb)) +* Implementation of select query parameter ([#69](https://github.wdf.sap.corp/AI/ai-api-client-sdk/issues/69)) ([56065ff](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/56065ff4c3a5bde5fe2e998d23f98db33b267b48)) +* updated doc string ([e0b5f36](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e0b5f36d09434f243dbbaca7b12823f336abae45)) + +# [1.17.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.16.0...v1.17.0) (2022-03-16) + + +### Bug Fixes + +* **model:** use scenario class instead of dict ([9adc834](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9adc83477aaf967f193a53778ae31a60f92b513e)) +* **model:** use scenario class instead of dict ([9822452](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9822452834e0cf33040677ab8e45658fb2c606f9)) +* **models:** address review comments ([9a9341e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9a9341e86d8408beb7e5fc760ea7cdf07576310a)) + + +### Features + +* **expand:** implement $expand=scenario for Client SDK ([7c8c418](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7c8c418368a12a52716f500acc0e26b0e977ae34)) + +# [1.16.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.15.0...v1.16.0) (2022-03-14) + + +### Bug Fixes + +* **resourcegroups:** fix resourcegroups client base path ([86825a0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/86825a0ad0e82c54e78a732c6d3d3d7bf0eeb177)) + + +### Features + +* **resourcegroups:** add resourcegroups client ([9d089ef](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9d089ef50f54383541b68b0f384d5e309a2d7016)) + +# [1.15.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.14.4...v1.15.0) (2022-03-01) + + +### Bug Fixes + +* **deployment:** validate deployment modify input ([22e7331](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/22e7331addb7d37dce8bb085704a3792451e0518)) + + +### Features + +* **deployment:** modify deployment with new configuration id ([d042887](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d0428877b1cdf04f89891c26b13859202f39208f)) + +## [1.14.4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.14.3...v1.14.4) (2022-02-21) + + +### Bug Fixes + +* **metrics:** fix metric default values ([e27b9be](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e27b9bec9a3e74d9427e645b015257a665ff89ed)) + +## [1.14.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.14.2...v1.14.3) (2022-02-18) + + +### Bug Fixes + +* **metrics:** fix metric to_dict ([c5d1aeb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c5d1aeb7505f7c9e0dd89d2d181f6d38bfcc3b2f)) + +## [1.14.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.14.1...v1.14.2) (2022-02-01) + + +### Bug Fixes + +* **requirements:** fix version restriction ([9f43ce2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9f43ce25a370f7aff1a53777db542a13a42faae0)) + +## [1.14.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.14.0...v1.14.1) (2022-01-31) + + +### Bug Fixes + +* **error-handling:** add status code and error msg to exception classes ([c6f4cab](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c6f4cab1611b917f0b998518aeaadef215bd3969)) +* **error-handling:** add status code when raising authenticator exception ([bed8dab](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bed8dab58091445ce68d20e7c1cd2c79b59d7ab8)) +* **error-handling:** include backend details in authentication responses ([c094661](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c0946616b08fb44d2bf0db75521751e3b1503c73)) +* **error-handling:** make arguments status code and error msg optional ([800b03b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/800b03b26e3866056d2df8b22e8dd47bb6b488ba)) + +# [1.14.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.13.2...v1.14.0) (2022-01-24) + + +### Features + +* **ci:** added docs to .whl ([a4f51f3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a4f51f38988412ab8bfe9a8077dcb37348c81c60)) +* **sdk:** generate new version ([#56](https://github.wdf.sap.corp/AI/ai-api-client-sdk/issues/56)) ([01c5e9c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/01c5e9c82ea7436481b2240aaef7c37b68fce9d3)) + +## [1.13.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.13.1...v1.13.2) (2021-12-10) + + +### Bug Fixes + +* ignore tests in wheel ([17f14ba](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/17f14bafc73b07574f8f0dcfde0585f1a1fd3662)) + +## [1.13.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.13.0...v1.13.1) (2021-12-06) + + +### Bug Fixes + +* **metadata:** add python 3.6 support ([bee5539](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bee5539c5f6ea4b681d42aed1353d8870e85eb84)) +* add support for python 3.6 ([dd88953](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/dd889537cb97ae18bfd1a0186a745f34f742944b)) + +# [1.13.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.12.3...v1.13.0) (2021-12-01) + + +### Bug Fixes + +* further metadata adjustment ([bd01982](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/bd0198297c32ea388eec90603c43896d6e2c3bd7)) +* pylint ([f0a7663](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f0a76633d0bfb5621bbfd022c3d7f36738752582)) +* **integration tests:** comment out provision/deprovision per test ([2a6f7d0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2a6f7d0758d841329b1c295f017e341169d1886d)) +* **pylint:** fix false positive ([e3d23df](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e3d23df9a65d90d550e5b7bbf12e6b1d550a7a44)) +* add license and metadata info ([fe86d11](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/fe86d1121c0d641a865873a72df6c6118e07ceaf)) +* pep8 style ([023fbf1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/023fbf16be7fee55b249146805a35c6b07457aea)) +* **metrics:** check if params is None ([e7c2252](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e7c2252f1515af856d11cad91810ab558f26b4d4)) +* **pylint:** ignore false pylint errors ([693acea](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/693aceae53150cf129e0cc256821f4faf1fd80f1)) +* **search:** fix search param name ([d3d250c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d3d250ca336dfe90cc41f051a90bb2f5009c7afc)) +* **test:** comment out resource group creation per test ([df6e596](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/df6e59658bb3ea0b42715255c439cd628c269ad4)) +* **test:** fix tests ([1f603d4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1f603d42e7e751e8fa0c69f876a3ac69500602fd)) +* **test:** revert changes to scenario e2e test ([3b06628](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3b06628f5deb0da440c03cabe5307d4599c8d383)) +* **tests:** fix scenario e2e test ([7af989e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7af989e69cf1cacceb2380c27713fa74b7bdadb6)) + + +### Features + +* **search:** search artifacts and configs ([ae283d4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ae283d41a52911785c0f71bcccda136b84f2214c)) + +## [1.12.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.12.2...v1.12.3) (2021-11-04) + + +### Bug Fixes + +* **rest_client:** fix typing ([91924c3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/91924c3cc9569cbc1d9d01d798b3893562cb32ee)) +* **typing:** Type all class properties explicitly ([ba7c65f](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ba7c65f83a7f278cf3019680f883b93a69028a1f)) + +## [1.12.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.12.1...v1.12.2) (2021-10-27) + + +### Bug Fixes + +* **integration-tests:** fix e2e scenario test ([d75580d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d75580d580c213776b35b69bf4a5ce2fa8bb1ad0)) +* **metric:** add to_dict to metric object ([161f406](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/161f406674fe57653fdcc20fda435dc1fd82d1ff)) + +## [1.12.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.12.0...v1.12.1) (2021-10-26) + + +### Bug Fixes + +* **dummy:** simple fix ([2bf7edb](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/2bf7edb6b609284af9c9932ee2bb46229567f282)) + +# [1.12.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.11.0...v1.12.0) (2021-09-09) + + +### Features + +* **ci:** dummy commit after ecc 1.11.x ([c2f90e2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c2f90e28893c6a663e34df635e89f1210b341ccc)) + +# [1.11.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.10.1...v1.11.0) (2021-09-08) + + +### Features + +* **statusTransitionTime:** introduce status transition time ([f9ec3ac](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f9ec3ac86bc931a74350f1252ba6445de539655d)) + +## [1.10.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.10.0...v1.10.1) (2021-09-03) + + +### Bug Fixes + +* **deployment:** remove configuration_id from patch ([0b48c82](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/0b48c826e6fd08f06bdd7ac65d1d36f6a6bc82c1)) + +# [1.10.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.9.0...v1.10.0) (2021-09-03) + + +### Bug Fixes + +* **datetime:** fix datetime format ([15b99d6](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/15b99d616e25db5c50ae9ec299dce3ba4e3d5530)) +* **datetime:** fix datetime parser ([d39eacd](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/d39eacd4c548b666f240c3fba7804ace1afa1601)) +* **query_params:** fix query params ([43a102d](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/43a102da94e0a0bd99ea78909fac6c376ab7b57f)) + + +### Features + +* **logs:** support querying logs ([980794c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/980794c57531ce003de7b1c423727c77d488dd2b)) + +# [1.9.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.8.0...v1.9.0) (2021-07-20) + + +### Bug Fixes + +* **artifacts:** fix typo ([4ffd6e1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4ffd6e10ce37b6c61bc04e43a661516fd29be339)) + + +### Features + +* **artifacts:** add kind `other` ([729d9c0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/729d9c02d0cd88690a37b866c69048087c0809a8)) + +# [1.8.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.7.0...v1.8.0) (2021-07-15) + + +### Bug Fixes + +* **delete:** add return in method definition ([401fe4b](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/401fe4ba760705d3d3525f26bdc2ed891de0417d)) +* **format:** correct doc format ([b1cfefd](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b1cfefd8f6756030db2482252f4123918c4dd745)) + + +### Features + +* **metrics:** delete metrics ([05083e9](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/05083e9fdc2020b6963f294e8f9b6c42b9b772a0)) + +# [1.7.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.6.0...v1.7.0) (2021-06-24) + + +### Features + +* **metrics:** deprecate filter parameter ([e668106](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/e6681068a98504d9bb797d8ad548e4760bab024d)) + +# [1.6.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.5.1...v1.6.0) (2021-06-18) + + +### Features + +* **ci:** dummy commit after ecc 1.5.x ([459faad](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/459faad55636e064cc4ba62043d65cc525b8457f)) + +## [1.5.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.5.0...v1.5.1) (2021-06-17) + + +### Bug Fixes + +* **docs:** fix documentation generation ([5b41d51](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5b41d519ed7413907e566a610adadec97a7de503)) + +# [1.5.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.4.0...v1.5.0) (2021-06-09) + + +### Features + +* **resource_clients:** add endpoints for counting and status filter ([cbec2c4](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/cbec2c417df3b72e5b40d8242adea15f7ed51f3c)) +* **resource_clients:** replace object CountResponse by int ([a2d086c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/a2d086cce9159962e5793f08814e5c587ce0e42e)) + +# [1.4.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.3.3...v1.4.0) (2021-06-04) + + +### Bug Fixes + +* **artifact_label_selector:** address review comments ([f499bee](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/f499bee97768483d592f711c07082260e9a7bc96)) +* **metrics:** use $filter instead of filter ([94a05ae](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/94a05ae4f303f9f7211db0493eb315ec02a9e504)) + + +### Features + +* **artifact_label_selector:** add artifact_label_selector query param ([4cd97d1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4cd97d1ebc3b521dae226549f996660599b633c8)) + +## [1.3.3](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.3.2...v1.3.3) (2021-06-02) + + +### Bug Fixes + +* **rest_client:** error.request_id might be None ([26b1b68](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/26b1b681365e77a10959a09b10856410da5d4e21)) + +## [1.3.2](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.3.1...v1.3.2) (2021-06-02) + + +### Bug Fixes + +* **docs:** address code reviews ([5097306](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/509730608a56bbfe950d51320eb9f44bd4703c75)) + +## [1.3.1](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.3.0...v1.3.1) (2021-05-10) + + +### Bug Fixes + +* **setup.py:** use manifest.in ([c45434a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/c45434a6c29c777b56aafa26b48d472a199d2d47)) + +# [1.3.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.2.0...v1.3.0) (2021-05-10) + + +### Bug Fixes + +* **doc:** move generated html to docs folder ([747bd48](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/747bd48a684cac7fff4712c4ca33dc157cd737fc)) +* **package:** add generated html to package ([aaf7104](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/aaf71047eda3a949ef2e5fa8f24b3b99f5efdf8b)) +* **package:** correct typo ([46ed079](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/46ed079cba59013cc4d16acb728ae0c639f154fd)) + + +### Features + +* **doc:** generate doc with pydoc ([950ebb5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/950ebb5b858c88adf909c1220cfd7840fd0bc6c9)) +* **pydoc:** use master of cicdtoolkit ([b8b632e](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b8b632eb2a095a3e2eda03cc52aeadebaaf6ae4f)) + +# [1.2.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.1.0...v1.2.0) (2021-05-06) + + +### Bug Fixes + +* **tests:** decrease number of deployments in tests ([4266dd5](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4266dd5eaeb3838a7109528e5c3c0a9f21b63cc2)) + + +### Features + +* **metrics:** introduce executionIds param for metrics ([9ce542a](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/9ce542a2eb561f54694271e6c6c20c4a5332477e)) + +# [1.1.0](https://github.wdf.sap.corp/AI/ai-api-client-sdk/compare/v1.0.0...v1.1.0) (2021-04-30) + + +### Bug Fixes + +* **review:** put back import ([b3ab326](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/b3ab32636291198fcd203eeb19763f08d320a736)) + + +### Features + +* **api:** post deployment and execution at root level ([1bfa435](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/1bfa4355e97bee761347b38aa69b54973c8476d2)) + +# 1.0.0 (2021-04-15) + + +### Bug Fixes + +* **e2e tests:** added e2e tests ([5d38a07](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/5d38a07c31ff26f1b0b0f7ac9bbdc74ea0cbfedb)) +* **makefile:** fix ([ed72508](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/ed725080fcc8e380c28d90e687732cd6f629bb71)) +* **makefile:** fix tabs ([800a58c](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/800a58c71647f9ffecdb3dee6289f08258e52c92)) +* **params:** fix list path parameters ([3cfd6d8](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/3cfd6d82b6effe876431e2d89d8ddfd12c7c084f)) +* **pylint:** fix pylint docker ([7c9e456](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/7c9e456f6f72c9b8a79b6ac1c30554be1c35aca6)) +* **README:** address code reviews ([4ed0ff7](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4ed0ff75dcf958ad011ac799c50a9fee10f58fd4)) + + +### Features + +* **ai-api-client-sdk:** initial implementation ([4284245](https://github.wdf.sap.corp/AI/ai-api-client-sdk/commit/4284245f49859fe1ccb02f5192c2dddf757ad8f9)) diff --git a/packages/base/docs/ai_api_client_sdk.ai_api_v2_client.html b/packages/base/docs/ai_api_client_sdk.ai_api_v2_client.html new file mode 100644 index 0000000..f316ffd --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.ai_api_v2_client.html @@ -0,0 +1,131 @@ + + + + +Python: module ai_api_client_sdk.ai_api_v2_client + + + + + +
 
ai_api_client_sdk.ai_api_v2_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/ai_api_v2_client.py
+

+

+ + + + + +
 
Modules
       
os
+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPIV2Client +
+
+
+

+ + + + + + + +
 
class AIAPIV2Client(builtins.object)
   AIAPIV2Client(
+    base_url: str,
+    auth_url: str = None,
+    client_id: str = None,
+    client_secret: str = None,
+    cert_str: str = None,
+    key_str: str = None,
+    cert_file_path: str = None,
+    key_file_path: str = None,
+    token_creator: Callable[[], str] = None,
+    resource_group: str = None,
+    connect_timeout=60,
+    num_request_retries=3,
+    **kwargs
+)

+The AIAPIV2Client is the class implemented to interact with the AI API server. The user can use its attributes
+corresponding to the resources, for interacting with endpoints related to that resource. (i.e.,
+aiapiv2client.scenario)

+:param base_url: Base URL of the AI API server. Should include the base path as well. (i.e., "<base_url>/scenarios"
+    should work)
+:type base_url: str
+:param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to
+    None
+:type auth_url: str, optional
+:param client_id: client id to be used for authorization, defaults to None
+:type client_id: str, optional
+:param client_secret: client secret to be used for authorization, defaults to None
+:type client_secret: str, optional
+:param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None
+:type cert_str: str, optional
+:param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None
+:type key_str: str, optional
+:param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter,
+    defaults to None
+:type cert_file_path: str, optional
+:param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter,
+    defaults to None
+:type key_file_path: str, optional
+:param token_creator: the function which returns the Bearer token, when called. Either this, or
+    auth_url & client_id & client_secret should be specified, defaults to None
+:type token_creator: Callable[[], str], optional
+:param resource_group: The default resource group which will be used while sending the requests to the server. If
+    not set, the resource_group should be specified with every request to the server, defaults to None
+:type resource_group: str, optional
+:param read_timeout: Read timeout for requests in seconds, defaults to 60s
+:type read_timeout: int
+:param connect_timeout: Connect timeout for requests in seconds, defaults to 60s
+:type connect_timeout: int
+:param num_request_retries: Number of retries for failing requests with http status code 429, 500, 502, 503 or 504,
+    defaults to 60s
+:type num_request_retries: int
+:param client_type: Client type header to be sent in the request, defaults to 'AI API Python SDK'
+:type client_type: str
 
 Methods defined here:
+
__init__( + self, + base_url: str, + auth_url: str = None, + client_id: str = None, + client_secret: str = None, + cert_str: str = None, + key_str: str = None, + cert_file_path: str = None, + key_file_path: str = None, + token_creator: Callable[[], str] = None, + resource_group: str = None, + connect_timeout=60, + num_request_retries=3, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       AUTH_PARAM_ERROR_MESSAGE = '\nFor authorization please provide either one of ... & key_str\n c. cert_file_path & key_file_path\n'
+Callable = typing.Callable
+SKIP_AUTH_ENV_VAR = 'SKIP_AUTHORIZATION'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.exception.html b/packages/base/docs/ai_api_client_sdk.exception.html new file mode 100644 index 0000000..67d8d47 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.exception.html @@ -0,0 +1,1068 @@ + + + + +Python: module ai_api_client_sdk.exception + + + + + +
 
ai_api_client_sdk.exception
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/exception.py
+

+

+ + + + + +
 
Classes
       
+
builtins.Exception(builtins.BaseException) +
+
+
AIAPIClientSDKException +
+
+
AIAPIAuthenticatorException +
+
+
AIAPIAuthenticatorAuthorizationException +
AIAPIAuthenticatorForbiddenException +
AIAPIAuthenticatorInvalidRequestException +
AIAPIAuthenticatorMethodNotAllowedException +
AIAPIAuthenticatorServerException +
AIAPIAuthenticatorTimeoutException +
+
+
AIAPIInvalidInputException +
+
+
AIAPIServerException +
+
+
AIAPIAuthorizationException +
AIAPIInvalidRequestException +
AIAPINotFoundException +
AIAPIPreconditionFailedException +
+
+
+
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorAuthorizationException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorAuthorizationException(error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with unauthorized when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorAuthorizationException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorException(AIAPIClientSDKException)
   AIAPIAuthenticatorException(status_code: int = None, error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator`
 
 
Method resolution order:
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, status_code: int = None, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorForbiddenException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorForbiddenException(error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with forbidden when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorForbiddenException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorInvalidRequestException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorInvalidRequestException(error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with bad request when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorInvalidRequestException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorMethodNotAllowedException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorMethodNotAllowedException(error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with method not allowed when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorMethodNotAllowedException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorServerException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorServerException(
+    status_code: int = None,
+    error_message: str = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with server error when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorServerException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, status_code: int = None, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthenticatorTimeoutException(AIAPIAuthenticatorException)
   AIAPIAuthenticatorTimeoutException(error_message: str = None)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.authenticator.Authenticator` if
+the XSUAA server responded with request timeout when trying to retrieve a token
 
 
Method resolution order:
+
AIAPIAuthenticatorTimeoutException
+
AIAPIAuthenticatorException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, error_message: str = None)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIAuthorizationException(AIAPIServerException)
   AIAPIAuthorizationException(
+    description: str,
+    error_message: str,
+    error_code: str = None,
+    request_id: str = None,
+    details: dict = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 401 response
+is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there
+for object definition
 
 
Method resolution order:
+
AIAPIAuthorizationException
+
AIAPIServerException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + error_message: str, + error_code: str = None, + request_id: str = None, + details: dict = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Methods inherited from AIAPIServerException:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from AIAPIServerException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIClientSDKException(builtins.Exception)
   AIAPIClientSDKException(
+    description: str,
+    status_code: int = None,
+    error_message: str = None
+)

+Base Exception class for AI API Client SDK exceptions
 
 
Method resolution order:
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + status_code: int = None, + error_message: str = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors defined here:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIInvalidInputException(AIAPIClientSDKException)
   AIAPIInvalidInputException(description: str)

+Exception type raised, when the provided input is invalid
 
 
Method resolution order:
+
AIAPIInvalidInputException
+
AIAPIClientSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, description: str)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from AIAPIClientSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIInvalidRequestException(AIAPIServerException)
   AIAPIInvalidRequestException(
+    description: str,
+    error_code: str,
+    error_message: str,
+    request_id: str,
+    details: dict = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 400 response
+is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there
+for object definition
 
 
Method resolution order:
+
AIAPIInvalidRequestException
+
AIAPIServerException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + error_code: str, + error_message: str, + request_id: str, + details: dict = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Methods inherited from AIAPIServerException:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from AIAPIServerException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPINotFoundException(AIAPIServerException)
   AIAPINotFoundException(
+    description: str,
+    error_code: str,
+    error_message: str,
+    request_id: str,
+    details: dict = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 404 response
+is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there
+for object definition
 
 
Method resolution order:
+
AIAPINotFoundException
+
AIAPIServerException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + error_code: str, + error_message: str, + request_id: str, + details: dict = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Methods inherited from AIAPIServerException:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from AIAPIServerException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIPreconditionFailedException(AIAPIServerException)
   AIAPIPreconditionFailedException(
+    description: str,
+    error_code: str,
+    error_message: str,
+    request_id: str,
+    details: dict = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient` if a 412 response
+is received from the server. This extends the :class:`ai_api_client_sdk.exception.AIAPIServerException`, refer there
+for object definition
 
 
Method resolution order:
+
AIAPIPreconditionFailedException
+
AIAPIServerException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + error_code: str, + error_message: str, + request_id: str, + details: dict = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Methods inherited from AIAPIServerException:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from AIAPIServerException:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AIAPIServerException(builtins.Exception)
   AIAPIServerException(
+    description: str,
+    status_code: int,
+    error_message: str,
+    error_code: str = None,
+    request_id: str = None,
+    details: dict = None
+)

+Exception type that is raised by the :class:`ai_api_client_sdk.helpers.rest_client.RestClient`, if a non-2XX
+response is received from the server.

+:param description: description of the exception
+:type description: str
+:param status_code: Status code of the response from the server
+:type status_code: int
+:param error_message: Error message received from the server
+:type error_message: str
+:param error_code: Error code received from the server, defaults to None
+:type error_code: str, optional
+:param request_id: ID of the request, the response belongs to, defaults to None
+:type request_id: str, optional
+:param details: Error details received from the server, defaults to None
+:type details: dict, optional
 
 
Method resolution order:
+
AIAPIServerException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + description: str, + status_code: int, + error_message: str, + error_code: str = None, + request_id: str = None, + details: dict = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.authenticator.html b/packages/base/docs/ai_api_client_sdk.helpers.authenticator.html new file mode 100644 index 0000000..1fbd98b --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.authenticator.html @@ -0,0 +1,110 @@ + + + + +Python: module ai_api_client_sdk.helpers.authenticator + + + + + +
 
ai_api_client_sdk.helpers.authenticator
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/authenticator.py
+

+

+ + + + + +
 
Modules
       
os
+
requests
+
tempfile
+
time
+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Authenticator +
+
+
+

+ + + + + + + +
 
class Authenticator(builtins.object)
   Authenticator(
+    auth_url: str,
+    client_id: str,
+    client_secret: str = None,
+    cert_str: str = None,
+    key_str: str = None,
+    cert_file_path: str = None,
+    key_file_path: str = None
+)

+Authenticator class is implemented to retrieve and cache the authorization token from the xsuaa server

+:param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token)
+:type auth_url: str
+:param client_id: client id to be used for authorization
+:type client_id: str
+:param client_secret: client secret to be used for authorization, either client_secret or
+    (cert_file_path and key_file_path) need to be provided, defaults to None
+:type client_secret: str, optional
+:param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None
+:type cert_str: str, optional
+:param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None
+:type key_str: str, optional
+:param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter,
+    defaults to None
+:type cert_file_path: str, optional
+:param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter,
+    defaults to None
+:type key_file_path: str, optional
 
 Methods defined here:
+
__init__( + self, + auth_url: str, + client_id: str, + client_secret: str = None, + cert_str: str = None, + key_str: str = None, + cert_file_path: str = None, + key_file_path: str = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
get_token(self) -> str
Retrieves the token from the xsuaa server or from cache when expiration date not reached.

+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthenticatorException` if an unexpected exception occurs while
+    trying to retrieve the token
+:return: The Bearer token
+:rtype: str
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       BASE_DELAY_FOR_TOKEN_RETRY = 0.3
+MAX_RETRY_ATTEMPTS_FOR_TOKEN = 3
+Optional = typing.Optional
+PARAM_ERROR_MESSAGE = 'Either client_secret, or (cert_file_path, key_fi..., or (cert_str, key_str) pair need to be provided'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.constants.html b/packages/base/docs/ai_api_client_sdk.helpers.constants.html new file mode 100644 index 0000000..f9506f7 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.constants.html @@ -0,0 +1,95 @@ + + + + +Python: module ai_api_client_sdk.helpers.constants + + + + + +
 
ai_api_client_sdk.helpers.constants
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/constants.py
+

+

+ + + + + +
 
Modules
       
re
+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
Timeouts +
+
+
+

+ + + + + + + +
 
class Timeouts(enum.Enum)
   Timeouts(*values)

+
 
 
Method resolution order:
+
Timeouts
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
NUM_REQUEST_RETRIES = <Timeouts.NUM_REQUEST_RETRIES: 3>
+ +
READ_TIMEOUT = <Timeouts.READ_TIMEOUT: 60>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ + + + + +
 
Data
       DEBUG_ENV_VAR_NAME = 'DEBUG'
+SCENARIO_LABEL_NAME_PATTERN = re.compile('^scenarios\\.ai\\.sap\\.com/[\\w.-]+$')
+SKIP_AUTH_ENV_VAR = 'SKIP_AUTHORIZATION'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.datetime_parser.html b/packages/base/docs/ai_api_client_sdk.helpers.datetime_parser.html new file mode 100644 index 0000000..8d1a273 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.datetime_parser.html @@ -0,0 +1,30 @@ + + + + +Python: module ai_api_client_sdk.helpers.datetime_parser + + + + + +
 
ai_api_client_sdk.helpers.datetime_parser
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/datetime_parser.py
+

+

+ + + + + +
 
Functions
       
parse_datetime(datetime_str: str) -> datetime.datetime
+

+ + + + + +
 
Data
       DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+DATETIME_FORMAT_36 = '%Y-%m-%dT%H:%M:%S+00:00'
+DATETIME_FORMAT_FLOAT = '%Y-%m-%dT%H:%M:%S.%fZ'
+DATETIME_FORMAT_FLOAT_TZ = '%Y-%m-%dT%H:%M:%S.%f+00:00'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.html b/packages/base/docs/ai_api_client_sdk.helpers.html new file mode 100644 index 0000000..bff6917 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.html @@ -0,0 +1,26 @@ + + + + +Python: package ai_api_client_sdk.helpers + + + + + +
 
ai_api_client_sdk.helpers
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/__init__.py
+

+

+ + + + + +
 
Package Contents
       
authenticator
+constants
+
datetime_parser
+llm_helper
+
logging
+rest_client
+
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.llm_helper.html b/packages/base/docs/ai_api_client_sdk.helpers.llm_helper.html new file mode 100644 index 0000000..452abea --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.llm_helper.html @@ -0,0 +1,32 @@ + + + + +Python: module ai_api_client_sdk.helpers.llm_helper + + + + + +
 
ai_api_client_sdk.helpers.llm_helper
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/llm_helper.py
+

+

+ + + + + +
 
Functions
       
check_if_llm_scenario(scenario)
+
filter_for_llm_scenarios(response_dict)
+
get_attr(obj, attr)
+

+ + + + + +
 
Data
       SCENARIO_LABEL_NAME_PATTERN = re.compile('^scenarios\\.ai\\.sap\\.com/[\\w.-]+$')
+get_key = functools.partial(get_attr, attr='key')
+get_labels = functools.partial(get_attr, attr='labels')
+get_value = functools.partial(get_attr, attr='value')
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.logging.html b/packages/base/docs/ai_api_client_sdk.helpers.logging.html new file mode 100644 index 0000000..a3e4032 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.logging.html @@ -0,0 +1,39 @@ + + + + +Python: module ai_api_client_sdk.helpers.logging + + + + + +
 
ai_api_client_sdk.helpers.logging
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/logging.py
+

+

+ + + + + +
 
Modules
       
logging
+
os
+

+ + + + + +
 
Functions
       
get_logger()
+
get_logger_name()
+
set_log_level(logger: logging.Logger)
+

+ + + + + +
 
Data
       DEBUG_ENV_VAR_NAME = 'DEBUG'
+DEFAULT_LOG_LEVEL = 20
+LOGGER_NAME = 'ai-api-client-sdk'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.helpers.rest_client.html b/packages/base/docs/ai_api_client_sdk.helpers.rest_client.html new file mode 100644 index 0000000..f78eac8 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.helpers.rest_client.html @@ -0,0 +1,247 @@ + + + + +Python: module ai_api_client_sdk.helpers.rest_client + + + + + +
 
ai_api_client_sdk.helpers.rest_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/helpers/rest_client.py
+

+

+ + + + + +
 
Modules
       
humps
+
json
+
os
+
requests
+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
RestClient +
+
+
+

+ + + + + + + +
 
class RestClient(builtins.object)
   RestClient(
+    base_url: str,
+    get_token: Callable[[], str],
+    resource_group: str = None,
+    client_type: str = None,
+    read_timeout=60,
+    connect_timeout=60,
+    num_request_retries=3
+)

+RestClient is the class implemented for sending the requests to the server.

+:param base_url: Base URL of the server. Should include the base path as well. (i.e., "<base_url>/scenarios" should
+    work)
+:type base_url: str
+:param get_token: the function which returns the Bearer token, when called
+:type get_token: Callable[[], str]
+:param resource_group: The default resource group which will be used while sending the requests to the server,
+    defaults to None
+:type resource_group: str
+:param client_type: Used for Metering to distinguish eg AI Launchpad python SDKs etc,
+    defaults to None
+:type client_type: str
+:param read_timeout: Read timeout for requests in seconds, defaults to 60s
+:type read_timeout: int
+:param connect_timeout: Connect timeout for requests in seconds, defaults to 60s
+:type connect_timeout: int
+:param num_request_retries: Number of retries for failing requests with http status code 429, 500, 502, 503 or 504,
+    defaults to 60s
+:type num_request_retries: int
 
 Methods defined here:
+
__init__( + self, + base_url: str, + get_token: Callable[[], str], + resource_group: str = None, + client_type: str = None, + read_timeout=60, + connect_timeout=60, + num_request_retries=3 +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
delete( + self, + path: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a DELETE request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param params: parameters of the request, defaults to None
+:type params: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
get( + self, + path: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + return_bytes_content: bool = False, + **kwargs +) -> Union[dict, int]
Sends a GET request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param params: parameters of the request, defaults to None
+:type params: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param return_bytes_content: expected response.content is bytes
+:type return_bytes_content: bool
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: Union[dict, int]
+ +
patch( + self, + path: str, + body: Dict[str, Union[str, dict, list]], + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a PATCH request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param body: body of the request
+:type body: Dict[str, Union[str, dict, list]]
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
post( + self, + path: str, + body: Dict[str, Union[str, dict]] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a POST request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param body: body of the request, defaults to None
+:type body: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
+Static methods defined here:
+
raise_ai_api_exception(error_description, response, response_json)
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
logger = <Logger ai-api-client-sdk (WARNING)>
+ +

+ + + + + +
 
Data
       Callable = typing.Callable
+Dict = typing.Dict
+SKIP_AUTH_ENV_VAR = 'SKIP_AUTHORIZATION'
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.html b/packages/base/docs/ai_api_client_sdk.html new file mode 100644 index 0000000..374a56f --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.html @@ -0,0 +1,25 @@ + + + + +Python: package ai_api_client_sdk + + + + + +
 
ai_api_client_sdk
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/__init__.py
+

+

+ + + + + +
 
Package Contents
       
ai_api_v2_client
+exception
+
helpers (package)
+models (package)
+
resource_clients (package)
+
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities.html new file mode 100644 index 0000000..1cdccd4 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities.html @@ -0,0 +1,117 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_capabilities + + + + + +
 
ai_api_client_sdk.models.ai_api_capabilities
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_capabilities.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPICapabilities +
+
+
+

+ + + + + + + +
 
class AIAPICapabilities(builtins.object)
   AIAPICapabilities(
+    multitenant: bool = True,
+    shareable: bool = True,
+    static_deployments: bool = True,
+    user_deployments: bool = True,
+    time_to_live_deployments: bool = False,
+    user_executions: bool = True,
+    bulk_updates: ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates = None,
+    execution_schedules: bool = False,
+    logs: ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs = None,
+    **kwargs
+)

+The AIAPICapabilities object represent the capabilities of the AI API

+:param multitenant: indicates whether resource groups are supported, defaults to True
+:type multitenant: bool, optional
+:param shareable: indicates whether clients can share an instance, defaults to True
+:type shareable: bool, optional
+:param static_deployments: indicates whether the static, always running deployments are supported, defaults to True
+:type static_deployments: bool, optional
+:param user_deployments: indicates whether deployment creation by users are supported, defaults to True
+:type user_deployments: bool, optional
+:param time_to_live_deployments: indicate whether ttl value of deployment are supported, defaults to False
+:type time_to_live_deployments: bool, optional
+:param user_executions: indicates whether execution creation by users are supported, defaults to True
+:type user_executions: bool, optional
+:param bulk_updates: An object, defining the bulk updates capabilities, defaults to None
+:type bulk_updates: class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates`,
+ optional
+:param execution_schedules: indicates whether execution schedules are supported, defaults to False
+:type execution_schedules: bool, optional
+:param logs: An object, defining the logs capabilities, defaults to None
+:type logs: class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + multitenant: bool = True, + shareable: bool = True, + static_deployments: bool = True, + user_deployments: bool = True, + time_to_live_deployments: bool = False, + user_executions: bool = True, + bulk_updates: ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates = None, + execution_schedules: bool = False, + logs: ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_capabilities_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilitiesobject, created from the
+values in the dict provided as parameter

+:param ai_api_capabilities_dict: Dict which includes the necessary values to create the object
+:type ai_api_capabilities_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.html new file mode 100644 index 0000000..a66d2fb --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.html @@ -0,0 +1,83 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_capabilities_bulk_updates + + + + + +
 
ai_api_client_sdk.models.ai_api_capabilities_bulk_updates
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_capabilities_bulk_updates.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPICapabilitiesBulkUpdates +
+
+
+

+ + + + + + + +
 
class AIAPICapabilitiesBulkUpdates(builtins.object)
   AIAPICapabilitiesBulkUpdates(
+    executions: bool = False,
+    deployments: bool = False,
+    **kwargs
+)

+The AIAPICapabilitiesBulkUpdates object represents the bulk updates capabilities

+:param deployments: indicates whether bulk updates for executions are supported, defaults to False
+:type deployments: bool, optional
+:param executions: indicates whether bulk updates for executions are supported, defaults to False
+:type executions: bool, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, executions: bool = False, deployments: bool = False, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_capabilities_bulk_updates_dict: Dict[str, bool])
Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdatesobject, created
+from the values in the dict provided as parameter

+:param ai_api_capabilities_bulk_updates_dict: Dict which includes the necessary values to create the object
+:type ai_api_capabilities_bulk_updates_dict: Dict[str, bool]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_capabilities_bulk_updates.AIAPICapabilitiesBulkUpdates`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_logs.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_logs.html new file mode 100644 index 0000000..cb6f4d8 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_capabilities_logs.html @@ -0,0 +1,83 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_capabilities_logs + + + + + +
 
ai_api_client_sdk.models.ai_api_capabilities_logs
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_capabilities_logs.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPICapabilitiesLogs +
+
+
+

+ + + + + + + +
 
class AIAPICapabilitiesLogs(builtins.object)
   AIAPICapabilitiesLogs(
+    executions: bool = True,
+    deployments: bool = True,
+    **kwargs
+)

+The AIAPICapabilitiesLogs object represents the log capabilities

+:param user_executions: indicates whether logs for executions are supported, defaults to True
+:type user_executions: bool, optional
+:param user_deployments: indicates whether logs for deployments are supported, defaults to True
+:type user_deployments: bool, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, executions: bool = True, deployments: bool = True, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_capabilities_logs_dict: Dict[str, bool])
Returns a :class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogsobject, created
+from the values in the dict provided as parameter

+:param ai_api_capabilities_logs_dict: Dict which includes the necessary values to create the object
+:type ai_api_capabilities_logs_dict: Dict[str, bool]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_capabilities_logs.AIAPICapabilitiesLogs`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_limits.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits.html new file mode 100644 index 0000000..457eed7 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits.html @@ -0,0 +1,88 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_limits + + + + + +
 
ai_api_client_sdk.models.ai_api_limits
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_limits.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPILimits +
+
+
+

+ + + + + + + +
 
class AIAPILimits(builtins.object)
   AIAPILimits(
+    executions: ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions = None,
+    deployments: ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments = None,
+    **kwargs
+)

+The AIAPILimits object represents the the limits for executions and deployments

+:param executions: represents the limits for executions, defaults to None
+:type executions: class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions`, optional
+:param deployments: represents the limits for deployments, defaults to None
+:type deployments: class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + executions: ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions = None, + deployments: ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_limits_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimitsobject, created from the values in the
+dict provided as parameter

+:param ai_api_limits_dict: Dict which includes the necessary values to create the object
+:type ai_api_limits_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimits`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_deployments.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_deployments.html new file mode 100644 index 0000000..4a4b356 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_deployments.html @@ -0,0 +1,85 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_limits_deployments + + + + + +
 
ai_api_client_sdk.models.ai_api_limits_deployments
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_limits_deployments.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments(builtins.object) +
+
+
AIAPILimitsDeployments +
+
+
+

+ + + + + + + +
 
class AIAPILimitsDeployments(ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments)
   AIAPILimitsDeployments(max_running_count: int = -1, **kwargs)

+The AIAPILimitsDeployments object represents the the limits for deployments

+:param max_running_count: max number of deployments per resource group, <0 means unlimited, defaults to -1
+:type max_running_count: int, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
AIAPILimitsDeployments
+
ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, max_running_count: int = -1, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_limits_deployments_dict: Dict[str, int])
Returns a :class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments` object, created
+from the values in the dict provided as parameter

+:param ai_api_limits_deployments_dict: Dict which includes the necessary values to create the object
+:type ai_api_limits_deployments_dict: Dict[str, int]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_limits_deployments.AIAPILimitsDeployments`
+ +
+Methods inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__eq__(self, other)
Return self==value.
+ +
+Data descriptors inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_enactments.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_enactments.html new file mode 100644 index 0000000..5afce7d --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_enactments.html @@ -0,0 +1,62 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_limits_enactments + + + + + +
 
ai_api_client_sdk.models.ai_api_limits_enactments
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_limits_enactments.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPILimitsEnactments +
+
+
+

+ + + + + + + +
 
class AIAPILimitsEnactments(builtins.object)
   AIAPILimitsEnactments(max_running_count: int = -1, **kwargs)

+The AIAPILimitsEnactments object represents the the limits for enactments (common for both executions
+and deployments)

+:param max_running_count: max number of enactments per resource group, <0 means unlimited, defaults to -1
+:type max_running_count: int, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, max_running_count: int = -1, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_executions.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_executions.html new file mode 100644 index 0000000..2662a10 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_limits_executions.html @@ -0,0 +1,85 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_limits_executions + + + + + +
 
ai_api_client_sdk.models.ai_api_limits_executions
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_limits_executions.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments(builtins.object) +
+
+
AIAPILimitsExecutions +
+
+
+

+ + + + + + + +
 
class AIAPILimitsExecutions(ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments)
   AIAPILimitsExecutions(max_running_count: int = -1, **kwargs)

+The AIAPILimitsExecutions object represents the the limits for executions

+:param max_running_count: max number of executions per resource group, <0 means unlimited, defaults to -1
+:type max_running_count: int, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
AIAPILimitsExecutions
+
ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, max_running_count: int = -1, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_limits_executions_dict: Dict[str, int])
Returns a :class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions` object, created
+from the values in the dict provided as parameter

+:param ai_api_limits_executions_dict: Dict which includes the necessary values to create the object
+:type ai_api_limits_executions_dict: Dict[str, int]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_limits_executions.AIAPILimitsExecutions`
+ +
+Methods inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__eq__(self, other)
Return self==value.
+ +
+Data descriptors inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.ai_api_limits_enactments.AIAPILimitsEnactments:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.ai_api_meta.html b/packages/base/docs/ai_api_client_sdk.models.ai_api_meta.html new file mode 100644 index 0000000..277b382 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.ai_api_meta.html @@ -0,0 +1,92 @@ + + + + +Python: module ai_api_client_sdk.models.ai_api_meta + + + + + +
 
ai_api_client_sdk.models.ai_api_meta
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/ai_api_meta.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AIAPIMeta +
+
+
+

+ + + + + + + +
 
class AIAPIMeta(builtins.object)
   AIAPIMeta(
+    version: str,
+    capabilities: ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities = None,
+    limits: ai_api_client_sdk.models.ai_api_limits.AIAPILimits = None,
+    **kwargs
+)

+The AIAPIMeta object represents the metadata and capabilities of the AI API

+:param version: version of the AI API
+:type version: str
+:param capabilities: capabilities of AI API, defaults to None
+:type capabilities: class:`ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities`
+:param limits: limits of AI API, defaults to None
+:type limits: class:`ai_api_client_sdk.models.ai_api_limits.AIAPILimits`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + version: str, + capabilities: ai_api_client_sdk.models.ai_api_capabilities.AIAPICapabilities = None, + limits: ai_api_client_sdk.models.ai_api_limits.AIAPILimits = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ai_api_meta_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMetaobject, created
+from the values in the dict provided as parameter

+:param ai_api_meta_dict: Dict which includes the necessary values to create the object
+:type ai_api_meta_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMeta`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.api_version.html b/packages/base/docs/ai_api_client_sdk.models.api_version.html new file mode 100644 index 0000000..5cb237d --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.api_version.html @@ -0,0 +1,92 @@ + + + + +Python: module ai_api_client_sdk.models.api_version + + + + + +
 
ai_api_client_sdk.models.api_version
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/api_version.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
APIVersion +
+
+
+

+ + + + + + + +
 
class APIVersion(builtins.object)
   APIVersion(
+    version_id: str = None,
+    url: str = None,
+    description: str = None,
+    **kwargs
+)

+The APIVersion object represents the description of an API version

+:param version_id: API version identifier, defaults to None
+:type version_id: str, optional
+:param url: URL of the API version, defaults to None
+:type url: str, optional
+:param description: API version description
+:type description: str, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + version_id: str = None, + url: str = None, + description: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(api_version_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.api_version.APIVersionobject, created from the
+values in the dict provided as parameter

+:param api_version_dict: Dict which includes the necessary values to create the object
+:type api_version_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.api_version.APIVersion`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.artifact.html b/packages/base/docs/ai_api_client_sdk.models.artifact.html new file mode 100644 index 0000000..3d205c3 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.artifact.html @@ -0,0 +1,131 @@ + + + + +Python: module ai_api_client_sdk.models.artifact + + + + + +
 
ai_api_client_sdk.models.artifact
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/artifact.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Artifact +
+
+
+

+ + + + + + + +
 
class Artifact(builtins.object)
   Artifact(
+    name: str,
+    id: str,
+    url: str,
+    kind: ai_api_client_sdk.models.artifact.Artifact.Kind,
+    scenario_id: str,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    execution_id: str = None,
+    configuration_id: str = None,
+    description: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None,
+    scenario: ai_api_client_sdk.models.scenario.Scenario = None,
+    **kwargs
+)

+The Artifact object defines an artifact

+:param name: Name of the artifact
+:type name: str
+:param id: ID of the artifact
+:type id: str
+:param url: URL of the artifact
+:type url: str
+:param kind: Kind of the artifact
+:type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`
+:param scenario_id: ID of the scenario which the artifact belongs to
+:type scenario_id: str
+:param created_at: Time when the artifact was created
+:type created_at: datetime
+:param modified_at: Time when the artifact was last modified
+:type modified_at: datetime
+:param execution_id: ID of the execution which the artifact resulted from, defaults to None
+:type execution_id: str, optional
+:param configuration_id: ID of the configuration which the artifact relates to, defaults to None
+:type configuration_id: str, optional
+:param description: Description of the artifact, defaults to None
+:type description: str, optional
+:param labels: List of the labels of the artifact, defaults to None
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
+:param scenario: A dict, which gives detailed information on scenario, defaults to None
+:type scenario: class:`ai_api_client_sdk.models.scenario.Scenario`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + id: str, + url: str, + kind: ai_api_client_sdk.models.artifact.Artifact.Kind, + scenario_id: str, + created_at: datetime.datetime, + modified_at: datetime.datetime, + execution_id: str = None, + configuration_id: str = None, + description: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + scenario: ai_api_client_sdk.models.scenario.Scenario = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(artifact_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.artifact.Artifactobject, created from the values in the dict
+provided as parameter

+:param artifact_dict: Dict which includes the necessary values to create the object
+:type artifact_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.artifact.Artifact`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
Kind = <enum 'Kind'>
+ +
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.artifact_create_response.html b/packages/base/docs/ai_api_client_sdk.models.artifact_create_response.html new file mode 100644 index 0000000..be8b014 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.artifact_create_response.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_api_client_sdk.models.artifact_create_response + + + + + +
 
ai_api_client_sdk.models.artifact_create_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/artifact_create_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.BasicResponse(builtins.object) +
+
+
ArtifactCreateResponse +
+
+
+

+ + + + + + + +
 
class ArtifactCreateResponse(ai_api_client_sdk.models.base_models.BasicResponse)
   ArtifactCreateResponse(id: str, message: str, url: str, **kwargs)

+The ArtifactCreateResponse object defines the response of the artifact create request
+:param id: ID of the artifact
+:type id: str
+:param message: Response message from the server
+:type message: str
+:param url: URL of the artifact
+:type url: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ArtifactCreateResponse
+
ai_api_client_sdk.models.base_models.BasicResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, id: str, message: str, url: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.artifact_query_response.html b/packages/base/docs/ai_api_client_sdk.models.artifact_query_response.html new file mode 100644 index 0000000..57bec6d --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.artifact_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.artifact_query_response + + + + + +
 
ai_api_client_sdk.models.artifact_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/artifact_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ArtifactQueryResponse +
+
+
+

+ + + + + + + +
 
class ArtifactQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ArtifactQueryResponse(
+    resources: List[ai_api_client_sdk.models.artifact.Artifact],
+    count: int,
+    **kwargs
+)

+The ArtifactQueryResponse object defines the response of the artifact query request
+:param resources: List of the artifacts returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.artifact.Artifact`]
+:param count: Total number of the queried artifacts
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ArtifactQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.artifact.Artifact], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.base_models.html b/packages/base/docs/ai_api_client_sdk.models.base_models.html new file mode 100644 index 0000000..a069ebf --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.base_models.html @@ -0,0 +1,488 @@ + + + + +Python: module ai_api_client_sdk.models.base_models + + + + + +
 
ai_api_client_sdk.models.base_models
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/base_models.py
+

+

+ + + + + +
 
Classes
       
+
aenum._enum.Enum(enum.Enum) +
+
+
Operation +
+
+
builtins.object +
+
+
BasicErrorResponse +
BasicModifyRequest +
BasicResponse +
BulkModifyErrorResponse +
KeyValue +
Name +
NameValue +
QueryResponse +
+
+
enum.Enum(builtins.object) +
+
+
Order +
+
+
+

+ + + + + + + +
 
class BasicErrorResponse(builtins.object)
   BasicErrorResponse(
+    code: str,
+    message: str,
+    request_id: str,
+    target: str,
+    details: Dict = None,
+    **kwargs
+)

+The BasicErrorResponse object defines the response from the server

+:param error_code: Error code from the server
+:type error_code: str
+:param message: Error message from the server
+:type message: str
+:param request_id: Request ID
+:type request_id: str  
+:param target: target
+:type target: str, optional
+:param details: Error details
+:type details: Dict, optional   
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + code: str, + message: str, + request_id: str, + target: str, + details: Dict = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(ber_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.base_models.BasicErrorResponseobject, created from the values 
+provided as parameter

+:param ber_dict: Dict which includes the necessary values to create the object
+:type ber_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicErrorResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class BasicModifyRequest(builtins.object)
   BasicModifyRequest(
+    id: str,
+    target_status: ai_api_client_sdk.models.target_status.TargetStatus,
+    **kwargs
+)

+The BasicModifyRequest object defines the request from client
+:param id: ID of the relevant resource
+:type id: str
+:param target_status: Target Status of the resource
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + id: str, + target_status: ai_api_client_sdk.models.target_status.TargetStatus, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
to_dict(self)
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class BasicResponse(builtins.object)
   BasicResponse(id: str, message: str, **kwargs)

+The BasicResponse object defines the response from the server

+:param id: ID of the relevant resource
+:type id: str
+:param message: Response message from the server
+:type message: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__(self, id: str, message: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(br_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.base_models.BasicResponseobject, created from the values in the
+dict provided as parameter

+:param br_dict: Dict which includes the necessary values to create the object
+:type br_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class BulkModifyErrorResponse(builtins.object)
   BulkModifyErrorResponse(
+    id: str,
+    error: ai_api_client_sdk.models.base_models.BasicErrorResponse,
+    **kwargs
+)

+The BulkModifyErrorResponse object defines the error response from the server
 
 Methods defined here:
+
__init__( + self, + id: str, + error: ai_api_client_sdk.models.base_models.BasicErrorResponse, + **kwargs +)
Creates an error object for a bulk modification call

+:param id: ID of the relevant resource
+:type id: str
+:param error: Error object response from the server
+:type error: BasicErrorResponse
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(error_dict: Dict[str, Union[str, Dict]])
:param error_dict: Dict which includes the necessary values to create the object
+:type error_dict: Dict[str, Union[str, Dict]]

+Returns:
+    Returns a class:`ai_api_client_sdk.models.base_models.BulkModifyErrorResponseobject
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class KeyValue(builtins.object)
   KeyValue(key: str, value: str, **kwargs)

+KeyValue object defines a key-value pair

+:param key: key of the pair
+:type key: str
+:param value: value of the pair
+:type value: str
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, key: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
to_dict(self)
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + + + +
 
class Name(builtins.object)
   Name(name: str, **kwargs)

+KeyValue object defines a name

+:param name: name
+:type name: str
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + + + +
 
class NameValue(builtins.object)
   NameValue(name: str, value: str, **kwargs)

+KeyValue object defines a name-value pair

+:param name: name of the pair
+:type name: str
+:param value: value of the pair
+:type value: str
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + + + +
 
class Operation(aenum._enum.Enum)
   Operation(*values)

+An enumeration.
 
 
Method resolution order:
+
Operation
+
aenum._enum.Enum
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
CASCADE_UPDATE = <Operation.CASCADE_UPDATE: 'CASCADE-UPDATE'>
+ +
CREATE = <Operation.CREATE: 'CREATE'>
+ +
DELETE = <Operation.DELETE: 'DELETE'>
+ +
UPDATE = <Operation.UPDATE: 'UPDATE'>
+ +
+Class methods inherited from aenum._enum.Enum:
+
__init_subclass__(**kwds) from aenum._enum
This method is called when a class is subclassed.

+The default implementation does nothing. It may be
+overridden to extend subclasses.
+ +
+Data descriptors inherited from aenum._enum.Enum:
+
name
+
+
name
+
+
value
+
+
value
+
+
values
+
+
+Static methods inherited from aenum._enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from aenum._enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases. Note that this
+is a copy of the internal mapping.
+
+

+ + + + + + + +
 
class Order(enum.Enum)
   Order(*values)

+
 
 
Method resolution order:
+
Order
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
ASC = <Order.ASC: 'asc'>
+ +
DESC = <Order.DESC: 'desc'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ + + + + + + +
 
class QueryResponse(builtins.object)
   QueryResponse(resources: list, count: int, **kwargs)

+The QueryResponse object defines the response from the server to a query request

+:param resources: List of the resources returned from the server
+:type resources: list
+:param count: Total number of the queried resources
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__(self, resources: list, count: int, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.capabilities.html b/packages/base/docs/ai_api_client_sdk.models.capabilities.html new file mode 100644 index 0000000..f318d79 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.capabilities.html @@ -0,0 +1,94 @@ + + + + +Python: module ai_api_client_sdk.models.capabilities + + + + + +
 
ai_api_client_sdk.models.capabilities
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/capabilities.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Capabilities +
+
+
+

+ + + + + + + +
 
class Capabilities(builtins.object)
   Capabilities(
+    ai_api: ai_api_client_sdk.models.ai_api_meta.AIAPIMeta,
+    runtime_identifier: str = None,
+    runtime_api_version: str = None,
+    description: str = None,
+    extensions: ai_api_client_sdk.models.extensions.Extensions = None,
+    **kwargs
+)

+The Capabilities object represents the metadata and capabilities of, and extensions to the AI API

+:param ai_api: Metadata and capabilities of the AI API
+:type ai_api: class:`ai_api_client_sdk.models.ai_api_meta.AIAPIMeta`
+:param runtime_identifier: The name of runtime, defaults to None
+:type runtime_identifier: str, optional
+:param runtime_api_version: The version of the runtime, defaults to None
+:type runtime_api_version: str, optional
+:param description: description, defaults to None
+:type description: str, optional
+:param extensions: Extensions to the AI API, defaults to None
+:type extensions: class:`ai_api_client_sdk.models.extensions.Extensions`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + ai_api: ai_api_client_sdk.models.ai_api_meta.AIAPIMeta, + runtime_identifier: str = None, + runtime_api_version: str = None, + description: str = None, + extensions: ai_api_client_sdk.models.extensions.Extensions = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(capabilities_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.capabilities.Capabilitiesobject, created
+from the values in the dict provided as parameter

+:param capabilities_dict: Dict which includes the necessary values to create the object
+:type capabilities_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.capabilities.Capabilities`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.configuration.html b/packages/base/docs/ai_api_client_sdk.models.configuration.html new file mode 100644 index 0000000..089765b --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.configuration.html @@ -0,0 +1,108 @@ + + + + +Python: module ai_api_client_sdk.models.configuration + + + + + +
 
ai_api_client_sdk.models.configuration
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/configuration.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Configuration +
+
+
+

+ + + + + + + +
 
class Configuration(builtins.object)
   Configuration(
+    id: str,
+    name: str,
+    scenario_id: str,
+    executable_id: str,
+    created_at: datetime.datetime,
+    parameter_bindings: List[ai_api_client_sdk.models.parameter_binding.ParameterBinding] = None,
+    input_artifact_bindings: List[ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding] = None,
+    scenario: ai_api_client_sdk.models.scenario.Scenario = None,
+    **kwargs
+)

+The Configuration object defines a configuration

+:param id: ID of the configuration
+:type id: str
+:param name: Name of the configuration
+:type name: str
+:param scenario_id: ID of the scenario which the configuration belongs to
+:type scenario_id: str
+:param executable_id: ID of the executable, which is configured
+:type executable_id: str
+:param created_at: Time when the configuration was created
+:type created_at: datetime
+:param parameter_bindings: List of the input parameters defined as key-value pairs, defaults to None
+:type parameter_bindings: List[class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding`], optional
+:param input_artifact_bindings: List of the input artifacts which are to be used by the executable, defaults to None
+:type input_artifact_bindings: List[class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding`],
+    optional
+:param scenario: A dict, which gives detailed information on scenario, defaults to None
+:type scenario: class:`ai_api_client_sdk.models.scenario.Scenario`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + id: str, + name: str, + scenario_id: str, + executable_id: str, + created_at: datetime.datetime, + parameter_bindings: List[ai_api_client_sdk.models.parameter_binding.ParameterBinding] = None, + input_artifact_bindings: List[ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding] = None, + scenario: ai_api_client_sdk.models.scenario.Scenario = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(configuration_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.configuration.Configurationobject, created from the values in the
+dict provided as parameter

+:param configuration_dict: Dict which includes the necessary values to create the object
+:type configuration_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.configuration.Configuration`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.configuration_create_response.html b/packages/base/docs/ai_api_client_sdk.models.configuration_create_response.html new file mode 100644 index 0000000..40d0605 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.configuration_create_response.html @@ -0,0 +1,74 @@ + + + + +Python: module ai_api_client_sdk.models.configuration_create_response + + + + + +
 
ai_api_client_sdk.models.configuration_create_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/configuration_create_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.BasicResponse(builtins.object) +
+
+
ConfigurationCreateResponse +
+
+
+

+ + + + + + + +
 
class ConfigurationCreateResponse(ai_api_client_sdk.models.base_models.BasicResponse)
   ConfigurationCreateResponse(id: str, message: str, **kwargs)

+The ConfigurationCreateResponse object defines the response of the configuration create request. Refer to
+:class:`ai_api_client_sdk.models.base_models.BasicResponse`, for the object definition
 
 
Method resolution order:
+
ConfigurationCreateResponse
+
ai_api_client_sdk.models.base_models.BasicResponse
+
builtins.object
+
+
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse` object,
+created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__init__(self, id: str, message: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.configuration_query_response.html b/packages/base/docs/ai_api_client_sdk.models.configuration_query_response.html new file mode 100644 index 0000000..3b8b254 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.configuration_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.configuration_query_response + + + + + +
 
ai_api_client_sdk.models.configuration_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/configuration_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ConfigurationQueryResponse +
+
+
+

+ + + + + + + +
 
class ConfigurationQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ConfigurationQueryResponse(
+    resources: List[ai_api_client_sdk.models.configuration.Configuration],
+    count: int,
+    **kwargs
+)

+The ConfigurationQueryResponse object defines the response of the configuration query request
+:param resources: List of the configurations returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.configuration.Configuration`]
+:param count: Total number of the queried configurations
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ConfigurationQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.configuration.Configuration], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse` object,
+created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.dataset_capabilities.html b/packages/base/docs/ai_api_client_sdk.models.dataset_capabilities.html new file mode 100644 index 0000000..65e16df --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.dataset_capabilities.html @@ -0,0 +1,92 @@ + + + + +Python: module ai_api_client_sdk.models.dataset_capabilities + + + + + +
 
ai_api_client_sdk.models.dataset_capabilities
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/dataset_capabilities.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
DatasetCapabilities +
+
+
+

+ + + + + + + +
 
class DatasetCapabilities(builtins.object)
   DatasetCapabilities(
+    upload: bool = True,
+    download: bool = True,
+    delete: bool = True,
+    **kwargs
+)

+The DatasetCapabilities object represents the capabilities of the Dataset API

+:param upload: indicates whether uploading files is supported, defaults to True
+:type upload: bool, optional
+:param download: indicates whether downloading files is supported, defaults to True
+:type download: bool, optional
+:param delete: indicates whether deleting files is supported, defaults to True
+:type delete: bool, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + upload: bool = True, + download: bool = True, + delete: bool = True, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(dataset_capabilities_dict: Dict[str, bool])
Returns a :class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilitiesobject, created from
+the values in the dict provided as parameter

+:param dataset_capabilities_dict: Dict which includes the necessary values to create the object
+:type dataset_capabilities_dict: Dict[str, bool]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.dataset_limits.html b/packages/base/docs/ai_api_client_sdk.models.dataset_limits.html new file mode 100644 index 0000000..84c9ee9 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.dataset_limits.html @@ -0,0 +1,88 @@ + + + + +Python: module ai_api_client_sdk.models.dataset_limits + + + + + +
 
ai_api_client_sdk.models.dataset_limits
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/dataset_limits.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
DatasetLimits +
+
+
+

+ + + + + + + +
 
class DatasetLimits(builtins.object)
   DatasetLimits(
+    max_upload_file_size: int = 104857600,
+    max_files_per_dataset: int = -1,
+    **kwargs
+)

+The DatasetLimits object represents the limits of the Dataset API

+:param max_upload_file_size: Max size (in bytes) of a single uploaded file, defaults to 104857600
+:type max_upload_file_size: int, optional
+:param max_files_per_dataset: Max number of files per dataset. <0 means unlimited, defaults to -1
+:type max_files_per_dataset: int, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + max_upload_file_size: int = 104857600, + max_files_per_dataset: int = -1, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(dataset_limits_dict: Dict[str, int])
Returns a :class:`ai_api_client_sdk.models.dataset_limits.DatasetLimitsobject, created from the values in
+the dict provided as parameter

+:param dataset_limits_dict: Dict which includes the necessary values to create the object
+:type dataset_limits_dict: Dict[str, int]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.dataset_limits.DatasetLimits`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.deployment.html b/packages/base/docs/ai_api_client_sdk.models.deployment.html new file mode 100644 index 0000000..0e2b167 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.deployment.html @@ -0,0 +1,160 @@ + + + + +Python: module ai_api_client_sdk.models.deployment + + + + + +
 
ai_api_client_sdk.models.deployment
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/deployment.py
+

+

+ + + + + +
 
Modules
       
humps
+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.enactment.Enactment(builtins.object) +
+
+
Deployment +
+
+
+

+ + + + + + + +
 
class Deployment(ai_api_client_sdk.models.enactment.Enactment)
   Deployment(
+    id: str,
+    deployment_url: str,
+    configuration_id: str,
+    configuration_name: str,
+    scenario_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    target_status: ai_api_client_sdk.models.target_status.TargetStatus,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    status_message: str = None,
+    status_details: Dict[str, Any] = None,
+    details: Dict[str, Any] = None,
+    submission_time: datetime.datetime = None,
+    start_time: datetime.datetime = None,
+    completion_time: datetime.datetime = None,
+    last_operation: ai_api_client_sdk.models.base_models.Operation = None,
+    latest_running_configuration_id: str = None,
+    ttl: str = None,
+    **kwargs
+)

+The Deployment object defines a deployment
+:param id: ID of the deployment
+:type id: str
+:param deployment_url: URL of the running deployment
+:type deployment_url: str
+:param configuration_id: ID of the configuration which configured the deployment
+:type configuration_id: str
+:param configuration_name: Name of the configuration which configured the deployment
+:type configuration_name: str
+:param scenario_id: ID of the scenario which the deployment belongs to
+:type scenario_id: str
+:param status: Status of the deployment
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param target_status: Target status of the deployment
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`
+:param created_at: Time when the deployment was created
+:type created_at: datetime
+:param modified_at: Time when the deployment was last modified
+:type modified_at: datetime
+:param status_message: A string, which gives information about the status of the deployment, defaults to None
+:type status_message: str, optional
+:param status_details: A dict, which gives detailed information about the status of the deployment, defaults to None
+:type status_details: Dict[str, Any], optional
+:param details: A dict, which gives information about the scaling and resources details of the deployment, defaults
+    to None
+:type details: Dict[str, Any], optional
+:param submission_time: Time when the deployment was submitted
+:type submission_time: datetime, optional
+:param start_time: Time when the deployment status changed to RUNNING
+:type start_time: datetime, optional
+:param completion_time: Time when the deployment status changed to DEAD/STOPPED
+:type completion_time: datetime, optional
+:param last_operation: Last operation applied to the deployment
+:type last_operation: Operation, optional
+:param latest_running_configuration_id: The configuration ID that was running, before a PATCH operation has modified
+    the configuration ID of the deployment.
+:type latest_running_configuration_id: str, optional
+:param ttl: Time to live for a deployment
+:type ttl: str, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
Deployment
+
ai_api_client_sdk.models.enactment.Enactment
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + deployment_url: str, + configuration_id: str, + configuration_name: str, + scenario_id: str, + status: ai_api_client_sdk.models.status.Status, + target_status: ai_api_client_sdk.models.target_status.TargetStatus, + created_at: datetime.datetime, + modified_at: datetime.datetime, + status_message: str = None, + status_details: Dict[str, Any] = None, + details: Dict[str, Any] = None, + submission_time: datetime.datetime = None, + start_time: datetime.datetime = None, + completion_time: datetime.datetime = None, + last_operation: ai_api_client_sdk.models.base_models.Operation = None, + latest_running_configuration_id: str = None, + ttl: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(deployment_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.deployment.Deployment` object, created from the values in the dict
+provided as parameter

+:param deployment_dict: Dict which includes the necessary values to create the object
+:type deployment_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.deployment.Deployment`
+ +
+Data descriptors inherited from ai_api_client_sdk.models.enactment.Enactment:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.deployment_bulk_modify_response.html b/packages/base/docs/ai_api_client_sdk.models.deployment_bulk_modify_response.html new file mode 100644 index 0000000..00d5478 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.deployment_bulk_modify_response.html @@ -0,0 +1,78 @@ + + + + +Python: module ai_api_client_sdk.models.deployment_bulk_modify_response + + + + + +
 
ai_api_client_sdk.models.deployment_bulk_modify_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/deployment_bulk_modify_response.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
DeploymentBulkModifyResponse +
+
+
+

+ + + + + + + +
 
class DeploymentBulkModifyResponse(builtins.object)
   DeploymentBulkModifyResponse(
+    deployments: List[Union[ai_api_client_sdk.models.base_models.BasicResponse, ai_api_client_sdk.models.base_models.BulkModifyErrorResponse]],
+    **kwargs
+)

+The DeploymentBulkModifyResponse object defines the response to the deployments bulk modify request
+:param deployments: Response to the bulk modify request of deployments
+:type deployments: List[Union[BasicResponse, BulkModifyErrorResponse]]
 
 Methods defined here:
+
__init__( + self, + deployments: List[Union[ai_api_client_sdk.models.base_models.BasicResponse, ai_api_client_sdk.models.base_models.BulkModifyErrorResponse]], + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]])
Returns a :class:`ai_api_client_sdk.models.Deployment_Bulk_Modify_Response.DeploymentBulkModifyResponse`
+object, created from the values provided as parameter

+:param response_dict: Which includes the necessary values to create the object
+:type response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.Deployment_Bulk_Modify_Response.DeploymentBulkModifyResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.deployment_create_response.html b/packages/base/docs/ai_api_client_sdk.models.deployment_create_response.html new file mode 100644 index 0000000..c719b0b --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.deployment_create_response.html @@ -0,0 +1,101 @@ + + + + +Python: module ai_api_client_sdk.models.deployment_create_response + + + + + +
 
ai_api_client_sdk.models.deployment_create_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/deployment_create_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.BasicResponse(builtins.object) +
+
+
DeploymentCreateResponse +
+
+
+

+ + + + + + + +
 
class DeploymentCreateResponse(ai_api_client_sdk.models.base_models.BasicResponse)
   DeploymentCreateResponse(
+    id: str,
+    message: str,
+    deployment_url: str,
+    status: ai_api_client_sdk.models.status.Status,
+    ttl: str = None,
+    **kwargs
+)

+The DeploymentCreateResponse object defines the response of the deployment create query
+:param id: ID of the deployment
+:type id: str
+:param message: Response message from the server
+:type message: str
+:param deployment_url: URL of the running deployment
+:type deployment_url: str
+:param status: Status of the deployment
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param ttl: Time to live for deployment
+:type ttl: str, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
DeploymentCreateResponse
+
ai_api_client_sdk.models.base_models.BasicResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + message: str, + deployment_url: str, + status: ai_api_client_sdk.models.status.Status, + ttl: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse` object,
+created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.deployment_get_status_response.html b/packages/base/docs/ai_api_client_sdk.models.deployment_get_status_response.html new file mode 100644 index 0000000..c4ef313 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.deployment_get_status_response.html @@ -0,0 +1,101 @@ + + + + +Python: module ai_api_client_sdk.models.deployment_get_status_response + + + + + +
 
ai_api_client_sdk.models.deployment_get_status_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/deployment_get_status_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse(builtins.object) +
+
+
DeploymentGetStatusResponse +
+
+
+

+ + + + + + + +
 
class DeploymentGetStatusResponse(ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse)
   DeploymentGetStatusResponse(
+    id: str,
+    configuration_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    status_details: Dict[str, Any] = None,
+    details: Dict[str, Any] = None
+)

+The DeploymentGetStatusResponse object defines the response of the deployment get status
+:param id: ID of the deployment
+:type id: str
+:param configuration_id: ID of the configuration which configured the deployment
+:type configuration_id: str
+:param status: Status of the deployment
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param created_at: Time when the deployment was created
+:type created_at: datetime
+:param modified_at: Time when the deployment was last modified
+:type modified_at: datetime
+:param status_details: A dict, which gives detailed information about the status of the deployment, defaults to None
+:type status_details: Dict[str, Any], optional
 
 
Method resolution order:
+
DeploymentGetStatusResponse
+
ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + configuration_id: str, + status: ai_api_client_sdk.models.status.Status, + created_at: datetime.datetime, + modified_at: datetime.datetime, + status_details: Dict[str, Any] = None, + details: Dict[str, Any] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(deployment_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse`
+object, created from the values in the dict provided as parameter
+:param deployment_dict: Dict which includes the necessary values to create the object
+:type deployment_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse`
+ +
+Data descriptors inherited from ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.deployment_query_response.html b/packages/base/docs/ai_api_client_sdk.models.deployment_query_response.html new file mode 100644 index 0000000..4c24576 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.deployment_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.deployment_query_response + + + + + +
 
ai_api_client_sdk.models.deployment_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/deployment_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
DeploymentQueryResponse +
+
+
+

+ + + + + + + +
 
class DeploymentQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   DeploymentQueryResponse(
+    resources: List[ai_api_client_sdk.models.deployment.Deployment],
+    count: int,
+    **kwargs
+)

+The DeploymentQueryResponse object defines the response of the deployment query request
+:param resources: List of the deployments returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.deployment.Deployment`]
+:param count: Total number of the queried deployments
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
DeploymentQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.deployment.Deployment], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse` object, created 
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.enactment.html b/packages/base/docs/ai_api_client_sdk.models.enactment.html new file mode 100644 index 0000000..106d478 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.enactment.html @@ -0,0 +1,116 @@ + + + + +Python: module ai_api_client_sdk.models.enactment + + + + + +
 
ai_api_client_sdk.models.enactment
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/enactment.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Enactment +
+
+
+

+ + + + + + + +
 
class Enactment(builtins.object)
   Enactment(
+    id: str,
+    configuration_id: str,
+    configuration_name,
+    scenario_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    target_status: ai_api_client_sdk.models.target_status.TargetStatus,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    status_message: str = None,
+    status_details: Dict[str, Any] = None,
+    submission_time: datetime.datetime = None,
+    start_time: datetime.datetime = None,
+    completion_time: datetime.datetime = None,
+    **kwargs
+)

+Enactment object is base class for Execution/Deployment, defining their common attributes

+:param id: ID of the enactment
+:type id: str
+:param configuration_id: ID of the configuration which configured the enactment
+:type configuration_id: str
+:param configuration_name: Name of the configuration which configured the enactment
+:type configuration_name: str
+:param scenario_id: ID of the scenario which the enactment belongs to
+:type scenario_id: str
+:param status: Status of the enactment
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param target_status: Target status of the enactment
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`
+:param created_at: Time when the enactment was created
+:type created_at: datetime
+:param modified_at: Time when the enactment was last modified
+:type modified_at: datetime
+:param status_message: A string, which gives information about the status of the enactment, defaults to None
+:type status_message: str, optional
+:param status_details: A dict, which gives detailed information about the status of the enactment, defaults to None
+:type status_details: Dict[str, Any], optional
+:param submission_time: Time when the enactment was submitted
+:type submission_time: datetime, optional
+:param start_time: Time when the enactment status changed to RUNNING
+:type start_time: datetime, optional
+:param completion_time: Time when the enactment status changed to COMPLETED/DEAD/STOPPED
+:type completion_time: datetime, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + id: str, + configuration_id: str, + configuration_name, + scenario_id: str, + status: ai_api_client_sdk.models.status.Status, + target_status: ai_api_client_sdk.models.target_status.TargetStatus, + created_at: datetime.datetime, + modified_at: datetime.datetime, + status_message: str = None, + status_details: Dict[str, Any] = None, + submission_time: datetime.datetime = None, + start_time: datetime.datetime = None, + completion_time: datetime.datetime = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.enactment_get_status_response.html b/packages/base/docs/ai_api_client_sdk.models.enactment_get_status_response.html new file mode 100644 index 0000000..6be4fb6 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.enactment_get_status_response.html @@ -0,0 +1,84 @@ + + + + +Python: module ai_api_client_sdk.models.enactment_get_status_response + + + + + +
 
ai_api_client_sdk.models.enactment_get_status_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/enactment_get_status_response.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
EnactmentGetStatusResponse +
+
+
+

+ + + + + + + +
 
class EnactmentGetStatusResponse(builtins.object)
   EnactmentGetStatusResponse(
+    id: str,
+    configuration_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    status_details: Dict[str, Any] = None
+)

+EnactmentGetStatusResponse object defines the response from the server to get status of Execution/Deployment
+:param id: ID of the enactment
+:type id: str
+:param configuration_id: ID of the configuration which configured the enactment
+:type configuration_id: str
+:param status: Status of the enactment
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param created_at: Time when the enactment was created
+:type created_at: datetime
+:param modified_at: Time when the enactment was last modified
+:type modified_at: datetime
+:param status_details: A dict, which gives detailed information about the status of the enactment, defaults to None
+:type status_details: Dict[str, Any], optional
 
 Methods defined here:
+
__init__( + self, + id: str, + configuration_id: str, + status: ai_api_client_sdk.models.status.Status, + created_at: datetime.datetime, + modified_at: datetime.datetime, + status_details: Dict[str, Any] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.executable.html b/packages/base/docs/ai_api_client_sdk.models.executable.html new file mode 100644 index 0000000..2e2ff5a --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.executable.html @@ -0,0 +1,128 @@ + + + + +Python: module ai_api_client_sdk.models.executable + + + + + +
 
ai_api_client_sdk.models.executable
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/executable.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Executable +
+
+
+

+ + + + + + + +
 
class Executable(builtins.object)
   Executable(
+    id: str,
+    scenario_id: str,
+    version_id: str,
+    name: str,
+    deployable: bool,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    description: str = None,
+    parameters: List[ai_api_client_sdk.models.parameter.Parameter] = None,
+    input_artifacts: List[ai_api_client_sdk.models.input_artifact.InputArtifact] = None,
+    output_artifacts: List[ai_api_client_sdk.models.output_artifact.OutputArtifact] = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None,
+    **kwargs
+)

+The Executable object defines an executable
+:param id: ID of the Executable
+:type id: str
+:param scenario_id: ID of the scenario which the executable belongs to
+:type scenario_id: str
+:param version_id: ID of the version of the scenario, the executable belongs to
+:type version_id: str
+:param name: Name of the executable
+:type name: str
+:param deployable: Flag which defines if the executable is deployable
+:type deployable: bool
+:param created_at: Time when the executable was created
+:type created_at: datetime
+:param modified_at: Time when the executable was last modified
+:type modified_at: datetime
+:param description: Description of the executable, defaults to None
+:type description: str, optional
+:param parameters: List of the parameters of the executable, defaults to None
+:type parameters: List[class:`ai_api_client_sdk.models.parameter.Parameter`], optional
+:param input_artifacts: List of the input artifacts which are to be used by the executable, defaults to None
+:type input_artifacts: List[class:`ai_api_client_sdk.models.input_artifact.InputArtifact`], optional
+:param output_artifacts: List of the artifacts to be created by the executable, defaults to None
+:type output_artifacts: List[class:`ai_api_client_sdk.models.output_artifact.OutputArtifact`], optional
+:param labels: List of the labels of the executable, defaults to None
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + id: str, + scenario_id: str, + version_id: str, + name: str, + deployable: bool, + created_at: datetime.datetime, + modified_at: datetime.datetime, + description: str = None, + parameters: List[ai_api_client_sdk.models.parameter.Parameter] = None, + input_artifacts: List[ai_api_client_sdk.models.input_artifact.InputArtifact] = None, + output_artifacts: List[ai_api_client_sdk.models.output_artifact.OutputArtifact] = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(executable_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.executable.Executableobject, created from the values in the dict
+provided as parameter

+:param executable_dict: Dict which includes the necessary values to create the object
+:type executable_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.executable.Executable`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.executable_query_response.html b/packages/base/docs/ai_api_client_sdk.models.executable_query_response.html new file mode 100644 index 0000000..0f55a60 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.executable_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.executable_query_response + + + + + +
 
ai_api_client_sdk.models.executable_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/executable_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ExecutableQueryResponse +
+
+
+

+ + + + + + + +
 
class ExecutableQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ExecutableQueryResponse(
+    resources: List[ai_api_client_sdk.models.executable.Executable],
+    count: int,
+    **kwargs
+)

+The ExecutableQueryResponse object defines the response of the executable query request
+:param resources: List of the executables returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.executable.Executable`]
+:param count: Total number of the queried executables
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ExecutableQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.executable.Executable], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse` object, created 
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution.html b/packages/base/docs/ai_api_client_sdk.models.execution.html new file mode 100644 index 0000000..8d89af2 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution.html @@ -0,0 +1,141 @@ + + + + +Python: module ai_api_client_sdk.models.execution + + + + + +
 
ai_api_client_sdk.models.execution
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.enactment.Enactment(builtins.object) +
+
+
Execution +
+
+
+

+ + + + + + + +
 
class Execution(ai_api_client_sdk.models.enactment.Enactment)
   Execution(
+    id: str,
+    configuration_id: str,
+    configuration_name: str,
+    scenario_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    target_status: ai_api_client_sdk.models.target_status.TargetStatus,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    output_artifacts: List[ai_api_client_sdk.models.artifact.Artifact] = None,
+    status_message: str = None,
+    status_details: Dict[str, Any] = None,
+    submission_time: datetime.datetime = None,
+    start_time: datetime.datetime = None,
+    execution_schedule_id: str = None,
+    completion_time: datetime.datetime = None,
+    **kwargs
+)

+The Execution object defines an execution

+:param id: ID of the execution
+:type id: str
+:param configuration_id: ID of the configuration which configured the execution
+:type configuration_id: str
+:param configuration_name: Name of the configuration which configured the execution
+:type configuration_name: str
+:param scenario_id: ID of the scenario which the execution belongs to
+:type scenario_id: str
+:param status: Status of the execution
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param target_status: Target status of the execution
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`
+:param execution_schedule_id: ID of the execution schedule, defaults to None
+:type execution_schedule_id: str, optional
+:param created_at: Time when the execution was created
+:type created_at: datetime
+:param modified_at: Time when the execution was last modified
+:type modified_at: datetime
+:param output_artifacts: List of the artifacts created by the execution, defaults to None
+:type output_artifacts: List[class:`ai_api_client_sdk.models.artifact.Artifact`], optional
+:param status_message: Gives information about the status of the execution, defaults to None
+:type status_message: str, optional
+:param status_details: A dict, which gives detailed information about the status of the execution, defaults to None
+:type status_details: Dict[str, Any], optional
+:param submission_time: Time when the execution was submitted
+:type submission_time: datetime, optional
+:param start_time: Time when the execution status changed to RUNNING
+:type start_time: datetime, optional
+:param completion_time: Time when the execution status changed to COMPLETED/DEAD/STOPPED
+:type completion_time: datetime, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
Execution
+
ai_api_client_sdk.models.enactment.Enactment
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + configuration_id: str, + configuration_name: str, + scenario_id: str, + status: ai_api_client_sdk.models.status.Status, + target_status: ai_api_client_sdk.models.target_status.TargetStatus, + created_at: datetime.datetime, + modified_at: datetime.datetime, + output_artifacts: List[ai_api_client_sdk.models.artifact.Artifact] = None, + status_message: str = None, + status_details: Dict[str, Any] = None, + submission_time: datetime.datetime = None, + start_time: datetime.datetime = None, + execution_schedule_id: str = None, + completion_time: datetime.datetime = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(execution_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution.Execution` object, created from the values in the dict
+provided as parameter

+:param execution_dict: Dict which includes the necessary values to create the object
+:type execution_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution.Execution`
+ +
+Data descriptors inherited from ai_api_client_sdk.models.enactment.Enactment:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_bulk_modify_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_bulk_modify_response.html new file mode 100644 index 0000000..7d9a938 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_bulk_modify_response.html @@ -0,0 +1,78 @@ + + + + +Python: module ai_api_client_sdk.models.execution_bulk_modify_response + + + + + +
 
ai_api_client_sdk.models.execution_bulk_modify_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_bulk_modify_response.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ExecutionBulkModifyResponse +
+
+
+

+ + + + + + + +
 
class ExecutionBulkModifyResponse(builtins.object)
   ExecutionBulkModifyResponse(
+    executions: List[Union[ai_api_client_sdk.models.base_models.BasicResponse, ai_api_client_sdk.models.base_models.BulkModifyErrorResponse]],
+    **kwargs
+)

+The ExecutionBulkModifyResponse object defines the response to the executions bulk modify request
+:param exeuctions: Response to the bulk modify request of executions
+:type exeuctions: List[Union[BasicResponse, BulkModifyErrorResponse]]
 
 Methods defined here:
+
__init__( + self, + executions: List[Union[ai_api_client_sdk.models.base_models.BasicResponse, ai_api_client_sdk.models.base_models.BulkModifyErrorResponse]], + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]])
Returns a :class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponseobject,
+created from the values provided as parameter

+:param response_dict: Which includes the necessary values to create the object
+:type response_dict: Dict[str, List[Dict[str, Union[str, Dict]]]]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_create_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_create_response.html new file mode 100644 index 0000000..43e66a2 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_create_response.html @@ -0,0 +1,93 @@ + + + + +Python: module ai_api_client_sdk.models.execution_create_response + + + + + +
 
ai_api_client_sdk.models.execution_create_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_create_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.BasicResponse(builtins.object) +
+
+
ExecutionCreateResponse +
+
+
+

+ + + + + + + +
 
class ExecutionCreateResponse(ai_api_client_sdk.models.base_models.BasicResponse)
   ExecutionCreateResponse(
+    id: str,
+    message: str,
+    status: ai_api_client_sdk.models.status.Status,
+    **kwargs
+)

+The ExecutionCreateResponse object defines the response of the execution create request
+:param id: ID of the execution
+:type id: str
+:param message: Response message from the server
+:type message: str
+:param status: Status of the execution
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ExecutionCreateResponse
+
ai_api_client_sdk.models.base_models.BasicResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + message: str, + status: ai_api_client_sdk.models.status.Status, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse` object,
+created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_get_status_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_get_status_response.html new file mode 100644 index 0000000..6fb2a67 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_get_status_response.html @@ -0,0 +1,99 @@ + + + + +Python: module ai_api_client_sdk.models.execution_get_status_response + + + + + +
 
ai_api_client_sdk.models.execution_get_status_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_get_status_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse(builtins.object) +
+
+
ExecutionGetStatusResponse +
+
+
+

+ + + + + + + +
 
class ExecutionGetStatusResponse(ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse)
   ExecutionGetStatusResponse(
+    id: str,
+    configuration_id: str,
+    status: ai_api_client_sdk.models.status.Status,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    status_details: Dict[str, Any] = None
+)

+The ExecutionGetStatusResponse object defines the response of the execution get status
+:param id: ID of the execution
+:type id: str
+:param configuration_id: ID of the configuration which configured the execution
+:type configuration_id: str
+:param status: Status of the execution
+:type status: class:`ai_api_client_sdk.models.status.Status`
+:param created_at: Time when the execution was created
+:type created_at: datetime
+:param modified_at: Time when the execution was last modified
+:type modified_at: datetime
+:param status_details: A dict, which gives detailed information about the status of the execution, defaults to None
+:type status_details: Dict[str, Any], optional
 
 
Method resolution order:
+
ExecutionGetStatusResponse
+
ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + id: str, + configuration_id: str, + status: ai_api_client_sdk.models.status.Status, + created_at: datetime.datetime, + modified_at: datetime.datetime, + status_details: Dict[str, Any] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(execution_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse` object,
+ created from the values in the dict provided as parameter
+:param execution_dict: Dict which includes the necessary values to create the object
+:type execution_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse`
+ +
+Data descriptors inherited from ai_api_client_sdk.models.enactment_get_status_response.EnactmentGetStatusResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_query_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_query_response.html new file mode 100644 index 0000000..7c54721 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.execution_query_response + + + + + +
 
ai_api_client_sdk.models.execution_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ExecutionQueryResponse +
+
+
+

+ + + + + + + +
 
class ExecutionQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ExecutionQueryResponse(
+    resources: List[ai_api_client_sdk.models.execution.Execution],
+    count: int,
+    **kwargs
+)

+The ExecutionQueryResponse object defines the response of the execution query request
+:param resources: List of the executions returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.execution.Execution`]
+:param count: Total number of the queried executions
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ExecutionQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.execution.Execution], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse` object, created 
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_schedule.html b/packages/base/docs/ai_api_client_sdk.models.execution_schedule.html new file mode 100644 index 0000000..f6180a1 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_schedule.html @@ -0,0 +1,110 @@ + + + + +Python: module ai_api_client_sdk.models.execution_schedule + + + + + +
 
ai_api_client_sdk.models.execution_schedule
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_schedule.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ExecutionSchedule +
+
+
+

+ + + + + + + +
 
class ExecutionSchedule(builtins.object)
   ExecutionSchedule(
+    id: str,
+    name: str,
+    cron: str,
+    configuration_id: str,
+    status: ai_api_client_sdk.models.status.ScheduleStatus,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    start: datetime.datetime = None,
+    end: datetime.datetime = None,
+    **kwargs
+)

+An Execution Schedule allows to trigger executions periodically

+:param id: ID of the execution schedule
+:type id: str
+:param name: Name of the execution schedule
+:type name: str
+:param cron: Cron defining the schedule to run the executions
+:type cron: str
+:param configuration_id: ID of the configuration for the execution schedule
+:type configuration_id: str
+:param status: status of the execution schedule
+:type status: str
+:param created_at: Time when the execution schedule was created
+:type created_at: datetime, optional
+:param modified_at: Time when the execution schedule was last modified
+:type modified_at: datetime, optional
+:param start: Start time of the execution schedule
+:type start: datetime, optional
+:param end: End time of the execution schedule
+:type end: datetime, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + id: str, + name: str, + cron: str, + configuration_id: str, + status: ai_api_client_sdk.models.status.ScheduleStatus, + created_at: datetime.datetime, + modified_at: datetime.datetime, + start: datetime.datetime = None, + end: datetime.datetime = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(execution_schedule_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_schedule.Schedule` object, created from the values in
+the dict provided as parameter

+:param execution_schedule_dict: Dict which includes the necessary values to create the object
+:type execution_schedule_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_schedule.Schedule`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_schedule_create_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_schedule_create_response.html new file mode 100644 index 0000000..3aea875 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_schedule_create_response.html @@ -0,0 +1,74 @@ + + + + +Python: module ai_api_client_sdk.models.execution_schedule_create_response + + + + + +
 
ai_api_client_sdk.models.execution_schedule_create_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_schedule_create_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.BasicResponse(builtins.object) +
+
+
ExecutionScheduleCreateResponse +
+
+
+

+ + + + + + + +
 
class ExecutionScheduleCreateResponse(ai_api_client_sdk.models.base_models.BasicResponse)
   ExecutionScheduleCreateResponse(id: str, message: str, **kwargs)

+The ExecutionScheduleCreateResponse object defines the response of the execution schedule create request.
+Refer to :class:`ai_api_client_sdk.models.base_models.BasicResponse`, for the object definition
 
 
Method resolution order:
+
ExecutionScheduleCreateResponse
+
ai_api_client_sdk.models.base_models.BasicResponse
+
builtins.object
+
+
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__init__(self, id: str, message: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.BasicResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.execution_schedule_query_response.html b/packages/base/docs/ai_api_client_sdk.models.execution_schedule_query_response.html new file mode 100644 index 0000000..3d80313 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.execution_schedule_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.execution_schedule_query_response + + + + + +
 
ai_api_client_sdk.models.execution_schedule_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/execution_schedule_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ExecutionScheduleQueryResponse +
+
+
+

+ + + + + + + +
 
class ExecutionScheduleQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ExecutionScheduleQueryResponse(
+    resources: List[ai_api_client_sdk.models.execution_schedule.ExecutionSchedule],
+    count: int,
+    **kwargs
+)

+The ExecutionScheduleQueryResponse object defines the response of the execution schedule query request
+:param resources: List of the execution schedules returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.execution_schedule.ExecutionSchedule`]
+:param count: Total number of the queried execution schedules
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ExecutionScheduleQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.execution_schedule.ExecutionSchedule], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.execution_schedule_query_response.ExecutionScheduleQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.execution_schedule_query_response.ExecutionScheduleQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.extensions.html b/packages/base/docs/ai_api_client_sdk.models.extensions.html new file mode 100644 index 0000000..e12ea47 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.extensions.html @@ -0,0 +1,93 @@ + + + + +Python: module ai_api_client_sdk.models.extensions + + + + + +
 
ai_api_client_sdk.models.extensions
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/extensions.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Extensions +
+
+
+

+ + + + + + + +
 
class Extensions(builtins.object)
   Extensions(
+    analytics: ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics = None,
+    resource_groups: ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups = None,
+    dataset: ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset = None,
+    **kwargs
+)

+The Extensions object represents the extensions to the AI API

+:param analytics: Metadata and capabilities of the Analytics API, defaults to None
+:type analytics: class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics`, optional
+:param resource_groups: Metadata and capabilities of the Resource Groups API, defaults to None
+:type resource_groups: class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups`,
+    optional
+:param dataset: Metadata and capabilities of the Dataset API, defaults to None
+:type dataset: class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + analytics: ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics = None, + resource_groups: ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups = None, + dataset: ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(extensions_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.extensions.Extensionsobject, created from the values in the
+dict provided as parameter

+:param extensions_dict: Dict which includes the necessary values to create the object
+:type extensions_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.extensions.Extensions`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.extensions_analytics.html b/packages/base/docs/ai_api_client_sdk.models.extensions_analytics.html new file mode 100644 index 0000000..37c549a --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.extensions_analytics.html @@ -0,0 +1,77 @@ + + + + +Python: module ai_api_client_sdk.models.extensions_analytics + + + + + +
 
ai_api_client_sdk.models.extensions_analytics
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/extensions_analytics.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ExtensionsAnalytics +
+
+
+

+ + + + + + + +
 
class ExtensionsAnalytics(builtins.object)
   ExtensionsAnalytics(version: str, **kwargs)

+The ExtensionsAnalytics object represents the metadata and capabilities of the Analytics API

+:param version: Version of the Analytics API
+:type version: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, version: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(extensions_analytics_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalyticsobject, created from
+the values in the dict provided as parameter

+:param extensions_analytics_dict: Dict which includes the necessary values to create the object
+:type extensions_analytics_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.extensions_analytics.ExtensionsAnalytics`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.extensions_dataset.html b/packages/base/docs/ai_api_client_sdk.models.extensions_dataset.html new file mode 100644 index 0000000..15d0b75 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.extensions_dataset.html @@ -0,0 +1,92 @@ + + + + +Python: module ai_api_client_sdk.models.extensions_dataset + + + + + +
 
ai_api_client_sdk.models.extensions_dataset
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/extensions_dataset.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ExtensionsDataset +
+
+
+

+ + + + + + + +
 
class ExtensionsDataset(builtins.object)
   ExtensionsDataset(
+    version: str,
+    capabilities: ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities = None,
+    limits: ai_api_client_sdk.models.dataset_limits.DatasetLimits = None,
+    **kwargs
+)

+The ExtensionsDataset object represents the metadata and capabilities of the Dataset API

+:param version: Version of the Dataset API
+:type version: str
+:param capabilities: Capabilities of the Dataset API, defaults to None
+:type capabilities: class:`ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities`, optional
+:param limits: Limits of the Dataset API, defaults to None
+:type limits: class:`ai_api_client_sdk.models.dataset_limits.DatasetLimits`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + version: str, + capabilities: ai_api_client_sdk.models.dataset_capabilities.DatasetCapabilities = None, + limits: ai_api_client_sdk.models.dataset_limits.DatasetLimits = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(extensions_dataset_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDatasetobject, created from the
+values in the dict provided as parameter

+:param extensions_dataset_dict: Dict which includes the necessary values to create the object
+:type extensions_dataset_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.extensions_dataset.ExtensionsDataset`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.extensions_resource_groups.html b/packages/base/docs/ai_api_client_sdk.models.extensions_resource_groups.html new file mode 100644 index 0000000..5be017e --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.extensions_resource_groups.html @@ -0,0 +1,77 @@ + + + + +Python: module ai_api_client_sdk.models.extensions_resource_groups + + + + + +
 
ai_api_client_sdk.models.extensions_resource_groups
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/extensions_resource_groups.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ExtensionsResourceGroups +
+
+
+

+ + + + + + + +
 
class ExtensionsResourceGroups(builtins.object)
   ExtensionsResourceGroups(version: str, **kwargs)

+The ExtensionsResourceGroups object represents the metadata and capabilities of the Resource Groups API

+:param version: Version of the Resource Groups API
+:type version: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, version: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(extensions_resource_groups_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroupsobject,
+created from the values in the dict provided as parameter

+:param extensions_resource_groups_dict: Dict which includes the necessary values to create the object
+:type extensions_resource_groups_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.extensions_resource_groups.ExtensionsResourceGroups`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.healthz_status.html b/packages/base/docs/ai_api_client_sdk.models.healthz_status.html new file mode 100644 index 0000000..fcccd7e --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.healthz_status.html @@ -0,0 +1,139 @@ + + + + +Python: module ai_api_client_sdk.models.healthz_status + + + + + +
 
ai_api_client_sdk.models.healthz_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/healthz_status.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
HealthzStatus +
+
+
enum.Enum(builtins.object) +
+
+
HealthStatus +
+
+
+

+ + + + + + + +
 
class HealthStatus(enum.Enum)
   HealthStatus(*values)

+
 
 
Method resolution order:
+
HealthStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
NOT_READY = <HealthStatus.NOT_READY: 'NOT READY'>
+ +
READY = <HealthStatus.READY: 'READY'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ + + + + + + +
 
class HealthzStatus(builtins.object)
   HealthzStatus(
+    status: ai_api_client_sdk.models.healthz_status.HealthStatus,
+    message: str,
+    **kwargs
+)

+The HealthzStatus object defines the response of the healthz endpoint
+:param status: Health status of the server
+:type status: class:`ai_api_client_sdk.models.healthz_status.HealthStatus`
+:param message: Response message from the server
+:type message: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + status: ai_api_client_sdk.models.healthz_status.HealthStatus, + message: str, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(healthz_status_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.healthz_status.HealthzStatusobject, created from the values in
+the dict provided as parameter

+:param healthz_status_dict: Dict which includes the necessary values to create the object
+:type healthz_status_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.healthz_status.HealthzStatus`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.html b/packages/base/docs/ai_api_client_sdk.models.html new file mode 100644 index 0000000..e9cf1ce --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.html @@ -0,0 +1,88 @@ + + + + +Python: package ai_api_client_sdk.models + + + + + +
 
ai_api_client_sdk.models
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/__init__.py
+

+

+ + + + + +
 
Package Contents
       
ai_api_capabilities
+ai_api_capabilities_bulk_updates
+ai_api_capabilities_logs
+ai_api_limits
+ai_api_limits_deployments
+ai_api_limits_enactments
+ai_api_limits_executions
+ai_api_meta
+api_version
+artifact
+artifact_create_response
+artifact_query_response
+base_models
+capabilities
+configuration
+configuration_create_response
+configuration_query_response
+
dataset_capabilities
+dataset_limits
+deployment
+deployment_bulk_modify_response
+deployment_create_response
+deployment_get_status_response
+deployment_query_response
+enactment
+enactment_get_status_response
+executable
+executable_query_response
+execution
+execution_bulk_modify_response
+execution_create_response
+execution_get_status_response
+execution_query_response
+execution_schedule
+
execution_schedule_create_response
+execution_schedule_query_response
+extensions
+extensions_analytics
+extensions_dataset
+extensions_resource_groups
+healthz_status
+input_artifact
+input_artifact_binding
+label
+log_response
+metric
+metric_custom_info
+metric_label
+metric_resource
+metric_tag
+metrics_query_response
+
model
+model_base_data_allowed_scenarios
+model_query_response
+model_version
+output_artifact
+parameter
+parameter_binding
+resource_group
+resource_group_query_response
+resource_group_status
+scenario
+scenario_query_response
+status
+target_status
+version
+version_list
+version_query_response
+
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.input_artifact.html b/packages/base/docs/ai_api_client_sdk.models.input_artifact.html new file mode 100644 index 0000000..0305cbe --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.input_artifact.html @@ -0,0 +1,93 @@ + + + + +Python: module ai_api_client_sdk.models.input_artifact + + + + + +
 
ai_api_client_sdk.models.input_artifact
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/input_artifact.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
InputArtifact +
+
+
+

+ + + + + + + +
 
class InputArtifact(builtins.object)
   InputArtifact(
+    name: str,
+    kind: str = None,
+    description: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None
+)

+The InputArtifact object defines the input artifact specified in the executable definition.
+:param name: name of artifact
+:type name: str
+:param kind: kind of artifact (Dataset, Model, ResultSet)
+:type kind: str
+:param description: description of artifact
+:type description: str
+:param labels: labels for artifact
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + kind: str = None, + description: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(input_artifact_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.input_artifact.InputArtifactobject, created from the values
+in the dict provided as parameter

+:param input_artifact_dict: Dict which includes the necessary values to create the object
+:type input_artifact_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.input_artifact.InputArtifact`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.input_artifact_binding.html b/packages/base/docs/ai_api_client_sdk.models.input_artifact_binding.html new file mode 100644 index 0000000..eeeb524 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.input_artifact_binding.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_api_client_sdk.models.input_artifact_binding + + + + + +
 
ai_api_client_sdk.models.input_artifact_binding
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/input_artifact_binding.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
InputArtifactBinding +
+
+
+

+ + + + + + + +
 
class InputArtifactBinding(builtins.object)
   InputArtifactBinding(key: str, artifact_id: str, **kwargs)

+The InputArtifactBinding object defines the input artifact specified in the configuration.

+:param key: matches the input artifact name in the executable definition
+:type key: str
+:param id: ID of the artifact
+:type id: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, key: str, artifact_id: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
to_dict(self) -> Dict[str, str]
Returns the attributes of the object as a dictionary

+:return: A dict, including all the attributes of the object
+:rtype: Dict[str, str]
+ +
+Static methods defined here:
+
from_dict(input_artifact_binding_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBindingobject, created from
+the values in the dict provided as parameter

+:param input_artifact_binding_dict: Dict which includes the necessary values to create the object
+:type input_artifact_binding_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.label.html b/packages/base/docs/ai_api_client_sdk.models.label.html new file mode 100644 index 0000000..305f950 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.label.html @@ -0,0 +1,84 @@ + + + + +Python: module ai_api_client_sdk.models.label + + + + + +
 
ai_api_client_sdk.models.label
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/label.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.KeyValue(builtins.object) +
+
+
Label +
+
+
+

+ + + + + + + +
 
class Label(ai_api_client_sdk.models.base_models.KeyValue)
   Label(key: str, value: str, **kwargs)

+The Label object defines a label as a key-value pair. Refer to
+:class:`ai_api_client_sdk.models.base_models.KeyValue`, for the object definition
 
 
Method resolution order:
+
Label
+
ai_api_client_sdk.models.base_models.KeyValue
+
builtins.object
+
+
+Methods defined here:
+
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(label_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.label.Label` object, created from the values in the dict provided
+as parameter

+:param label_dict: Dict which includes the necessary values to create the object
+:type label_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.label.Label`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, key: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
to_dict(self)
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.log_response.html b/packages/base/docs/ai_api_client_sdk.models.log_response.html new file mode 100644 index 0000000..f60d310 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.log_response.html @@ -0,0 +1,157 @@ + + + + +Python: module ai_api_client_sdk.models.log_response + + + + + +
 
ai_api_client_sdk.models.log_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/log_response.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
LogResponse +
LogResponseData +
LogResultItem +
+
+
+

+ + + + + + + +
 
class LogResponse(builtins.object)
   LogResponse(
+    data: ai_api_client_sdk.models.log_response.LogResponseData,
+    **kwargs
+)

+The LogResponse object defines the response of the log request
+:param data: log response data
+:type data: class:`ai_api_client_sdk.models.log_response.LogResponseData`
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + data: ai_api_client_sdk.models.log_response.LogResponseData, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(log_response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.log_response.LogResponseobject, created from the values
+in the dict provided as parameter

+:param log_response_dict: Dict which includes the necessary values to create the object
+:type log_response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.log_response.LogResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class LogResponseData(builtins.object)
   LogResponseData(
+    result: List[ai_api_client_sdk.models.log_response.LogResultItem],
+    **kwargs
+)

+The LogResponseData object defines the data of the log response
+:param result: result of the log query
+:type result: List[class:`ai_api_client_sdk.models.log_response.LogResponseResultItem`]
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + result: List[ai_api_client_sdk.models.log_response.LogResultItem], + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(log_response_data_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.log_response.LogResponseDataobject, created from the values
+in the dict provided as parameter

+:param log_response_data_dict: Dict which includes the necessary values to create the object
+:type log_response_data_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.log_response.LogResponseData`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class LogResultItem(builtins.object)
   LogResultItem(msg: str, timestamp: datetime.datetime, **kwargs)

+The LogResultItem object defines each item in the log response
+:param msg: log message
+:type msg: str
+:param timestamp: timestamp corresponding to the log message
+:type timestamp: datetime
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__(self, msg: str, timestamp: datetime.datetime, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(log_result_item_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.log_response.LogResultItemobject, created from the values
+in the dict provided as parameter

+:param log_result_item_dict: Dict which includes the necessary values to create the object
+:type log_result_item_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.log_response.LogResultItem`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metric.html b/packages/base/docs/ai_api_client_sdk.models.metric.html new file mode 100644 index 0000000..3a93157 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metric.html @@ -0,0 +1,107 @@ + + + + +Python: module ai_api_client_sdk.models.metric + + + + + +
 
ai_api_client_sdk.models.metric
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metric.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Metric +
+
+
+

+ + + + + + + +
 
class Metric(builtins.object)
   Metric(
+    name: str,
+    value: float,
+    timestamp: datetime.datetime,
+    step: int = None,
+    labels: List[ai_api_client_sdk.models.metric_label.MetricLabel] = None,
+    **kwargs
+)

+The Metric object, defines a single metric.

+:param name: Name of the metric
+:type name: str
+:param value: numeric value of the metric
+:type value: float
+:param timestamp: Time when the metric was created
+:type timestamp: datetime
+:param step: any measurement of training progress (number of training iterations, number of epochs, etc.)
+:type step: int
+:param labels: List of the labels of the metric, defaults to None
+:type labels: List[class:`ai_api_client_sdk.models.metric_label.MetricLabel`]
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + value: float, + timestamp: datetime.datetime, + step: int = None, + labels: List[ai_api_client_sdk.models.metric_label.MetricLabel] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
to_dict(self)
Returns the attributes of the object as a dictionary

+:return: A dict, including all the attributes of the object
+:rtype: Dict[str, str]
+ +
+Static methods defined here:
+
from_dict(metric_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.metric.Metricobject, created from the values in the dict
+provided as parameter

+:param metric_dict: Dict which includes the necessary values to create the object
+:type metric_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metric.Metric`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metric_custom_info.html b/packages/base/docs/ai_api_client_sdk.models.metric_custom_info.html new file mode 100644 index 0000000..33c1f07 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metric_custom_info.html @@ -0,0 +1,81 @@ + + + + +Python: module ai_api_client_sdk.models.metric_custom_info + + + + + +
 
ai_api_client_sdk.models.metric_custom_info
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metric_custom_info.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.NameValue(builtins.object) +
+
+
MetricCustomInfo +
+
+
+

+ + + + + + + +
 
class MetricCustomInfo(ai_api_client_sdk.models.base_models.NameValue)
   MetricCustomInfo(name: str, value: str, **kwargs)

+The MetricCustomInfo object defines rendering/semantic information regarding certain metric for consuming
+application or complex metrics in JSON format, as a name-value pair. Refer to
+:class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition
 
 
Method resolution order:
+
MetricCustomInfo
+
ai_api_client_sdk.models.base_models.NameValue
+
builtins.object
+
+
+Static methods defined here:
+
from_dict(metric_custom_info_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo` object, created from the
+values in the dict provided as parameter

+:param metric_custom_info_dict: Dict which includes the necessary values to create the object
+:type metric_custom_info_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metric_label.html b/packages/base/docs/ai_api_client_sdk.models.metric_label.html new file mode 100644 index 0000000..34d5ddd --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metric_label.html @@ -0,0 +1,87 @@ + + + + +Python: module ai_api_client_sdk.models.metric_label + + + + + +
 
ai_api_client_sdk.models.metric_label
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metric_label.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.NameValue(builtins.object) +
+
+
MetricLabel +
+
+
+

+ + + + + + + +
 
class MetricLabel(ai_api_client_sdk.models.base_models.NameValue)
   MetricLabel(name: str, value: str, **kwargs)

+The MetricLabel object defines a metric label as a name-value pair. Refer to
+:class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition
 
 
Method resolution order:
+
MetricLabel
+
ai_api_client_sdk.models.base_models.NameValue
+
builtins.object
+
+
+Methods defined here:
+
to_dict(self)
Returns the attributes of the object as a dictionary

+:return: A dict, including all the attributes of the object
+:rtype: Dict[str, str]
+ +
+Static methods defined here:
+
from_dict(metric_label_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.metric_label.MetricLabel` object, created from the values in the
+dict provided as parameter

+:param metric_label_dict: Dict which includes the necessary values to create the object
+:type metric_label_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metric_label.MetricLabel`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metric_resource.html b/packages/base/docs/ai_api_client_sdk.models.metric_resource.html new file mode 100644 index 0000000..0096b87 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metric_resource.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_api_client_sdk.models.metric_resource + + + + + +
 
ai_api_client_sdk.models.metric_resource
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metric_resource.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
MetricResource +
+
+
+

+ + + + + + + +
 
class MetricResource(builtins.object)
   MetricResource(
+    execution_id: str,
+    metrics: List[ai_api_client_sdk.models.metric.Metric] = None,
+    tags: List[ai_api_client_sdk.models.metric_tag.MetricTag] = None,
+    custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo] = None,
+    **kwargs
+)

+The Metric object, defines collection of various metrics/tags/labels related to an execution.

+:param execution_id: ID of the execution
+:type execution_id: str
+:param metrics: List of the metrics related to the execution, defaults to None
+:type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional
+:param tags: List of the tags related to the execution, defaults to None
+:type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional
+:param custom_info: List of custom info related to the execution, defaults to None
+:type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + execution_id: str, + metrics: List[ai_api_client_sdk.models.metric.Metric] = None, + tags: List[ai_api_client_sdk.models.metric_tag.MetricTag] = None, + custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(metric_resource_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.metric_resource.MetricResourceobject, created from the values
+in the dict provided as parameter

+:param metric_resource_dict: Dict which includes the necessary values to create the object
+:type metric_resource_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metric_resource.MetricResource`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metric_tag.html b/packages/base/docs/ai_api_client_sdk.models.metric_tag.html new file mode 100644 index 0000000..1f27572 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metric_tag.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_api_client_sdk.models.metric_tag + + + + + +
 
ai_api_client_sdk.models.metric_tag
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metric_tag.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.NameValue(builtins.object) +
+
+
MetricTag +
+
+
+

+ + + + + + + +
 
class MetricTag(ai_api_client_sdk.models.base_models.NameValue)
   MetricTag(name: str, value: str, **kwargs)

+The MetricTag object defines a tag as a name-value pair. Refer to
+:class:`ai_api_client_sdk.models.base_models.NameValue`, for the object definition
 
 
Method resolution order:
+
MetricTag
+
ai_api_client_sdk.models.base_models.NameValue
+
builtins.object
+
+
+Methods defined here:
+
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(metric_tag_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.metric_tag.MetricTag` object, created from the values in the dict
+provided as parameter

+:param metric_tag_dict: Dict which includes the necessary values to create the object
+:type metric_tag_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metric_tag.MetricTag`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.NameValue:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.metrics_query_response.html b/packages/base/docs/ai_api_client_sdk.models.metrics_query_response.html new file mode 100644 index 0000000..49cc05c --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.metrics_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.metrics_query_response + + + + + +
 
ai_api_client_sdk.models.metrics_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/metrics_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
MetricsQueryResponse +
+
+
+

+ + + + + + + +
 
class MetricsQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   MetricsQueryResponse(
+    resources: List[ai_api_client_sdk.models.metric_resource.MetricResource],
+    count: int,
+    **kwargs
+)

+The MetricsQueryResponse object defines the response of the metrics query request
+:param resources: List of the metrics returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.metrics_resource.MetricResource`]
+:param count: Total number of the queried metrics
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
MetricsQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.metric_resource.MetricResource], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.model.html b/packages/base/docs/ai_api_client_sdk.models.model.html new file mode 100644 index 0000000..3bb89b9 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.model.html @@ -0,0 +1,105 @@ + + + + +Python: module ai_api_client_sdk.models.model + + + + + +
 
ai_api_client_sdk.models.model
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/model.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Model +
+
+
+

+ + + + + + + +
 
class Model(builtins.object)
   Model(
+    executable_id: str,
+    model: str,
+    description: str = None,
+    versions: List[ai_api_client_sdk.models.model_version.ModelVersion] = None,
+    display_name: str = None,
+    access_type: str = None,
+    provider: str = None,
+    allowed_scenarios: List[ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios] = None,
+    **kwargs
+)

+The Model object defines a model
+:param executable_id: ID of the executable
+:type executable_id: str
+:param model: Unique name of the model
+:type model: str
+:param description: Description of the model, defaults to None
+:type description: str, optional
+:param versions: List of available model versions, defaults to None
+:type versions: List[class:`ai_api_client_sdk.models.model_version.ModelVersion`], optional
+:param display_name: Display name of the model, defaults to None
+:type display_name: str, optional
+:param access_type: Access type of the model, defaults to None
+:type access_type: str, optional
+:param provider: Provider of the model, defaults to None
+:type provider: str, optional
+:param allowed_scenarios: List of allowed scenarios for the model, defaults to None
+:type allowed_scenarios:
+    List[ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios], optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + executable_id: str, + model: str, + description: str = None, + versions: List[ai_api_client_sdk.models.model_version.ModelVersion] = None, + display_name: str = None, + access_type: str = None, + provider: str = None, + allowed_scenarios: List[ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(model_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.model.Modelobject, created from the values in the dict
+provided as parameter

+:param model_dict: Dict which includes the necessary values to create the object
+:type model_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.model.Model`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.model_base_data_allowed_scenarios.html b/packages/base/docs/ai_api_client_sdk.models.model_base_data_allowed_scenarios.html new file mode 100644 index 0000000..b6e3f44 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.model_base_data_allowed_scenarios.html @@ -0,0 +1,62 @@ + + + + +Python: module ai_api_client_sdk.models.model_base_data_allowed_scenarios + + + + + +
 
ai_api_client_sdk.models.model_base_data_allowed_scenarios
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/model_base_data_allowed_scenarios.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ModelBaseDataAllowedScenarios +
+
+
+

+ + + + + + + +
 
class ModelBaseDataAllowedScenarios(builtins.object)
   ModelBaseDataAllowedScenarios(scenario_id: str, executable_id: str)

+This class defines the allowed scenarios for the model base data.
 
 Methods defined here:
+
__init__(self, scenario_id: str, executable_id: str)
:param scenario_id: ID of the scenario
+:type scenario_id: str
+:param executable_id: ID of the executable
+:type executable_id: str
+ +
+Static methods defined here:
+
from_dict(allowed_scenarios_dict: dict[str, any])
Returns a :class:`ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios`
+object, created from the values in the dict provided as parameter

+:param allowed_scenarios_dict: Dict which includes the necessary values to create the object
+:type allowed_scenarios_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.model_base_data_allowed_scenarios.ModelBaseDataAllowedScenarios`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.model_query_response.html b/packages/base/docs/ai_api_client_sdk.models.model_query_response.html new file mode 100644 index 0000000..da7be2c --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.model_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.model_query_response + + + + + +
 
ai_api_client_sdk.models.model_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/model_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ModelQueryResponse +
+
+
+

+ + + + + + + +
 
class ModelQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ModelQueryResponse(
+    resources: List[ai_api_client_sdk.models.model.Model],
+    count: int,
+    **kwargs
+)

+The ModelQueryResponse object defines the response of the model query request
+:param resources: List of the models returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.executable.Model`]
+:param count: Total number of the queried models
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ModelQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.model.Model], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.model_version.html b/packages/base/docs/ai_api_client_sdk.models.model_version.html new file mode 100644 index 0000000..bbf3cdc --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.model_version.html @@ -0,0 +1,127 @@ + + + + +Python: module ai_api_client_sdk.models.model_version + + + + + +
 
ai_api_client_sdk.models.model_version
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/model_version.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ModelVersion +
+
+
+

+ + + + + + + +
 
class ModelVersion(builtins.object)
   ModelVersion(
+    name: str,
+    is_latest: bool,
+    deprecated: bool,
+    retirement_date: Optional[datetime.datetime] = None,
+    context_length: Optional[int] = None,
+    input_types: Optional[List[str]] = None,
+    capabilities: Optional[List[str]] = None,
+    metadata: Optional[Dict[str, str]] = None,
+    cost: Optional[Dict[str, str]] = None,
+    suggested_replacements: Optional[List[str]] = None,
+    streaming_supported: Optional[bool] = None,
+    orchestration_capabilities: Optional[List[str]] = None,
+    **kwargs
+)

+The ModelVersion object defines a version for a model
+:param name: Name of the model version
+:type name: str
+:param is_latest: True if model version is latest, otherwise false
+:type is_latest: bool
+:param deprecated: True if model version is deprecated, otherwise false
+:type deprecated: bool
+:param retirement_date: Retirement date of the model version, defaults to None
+:type retirement_date: datetime, optional
+:param context_length: Context length of the model version, defaults to None
+:type context_length: int, optional
+:param input_types: Input types supported by the model version, defaults to None
+:type input_types: List[str], optional
+:param capabilities: Capabilities of the model version, defaults to None
+:type capabilities: List[str], optional
+:param metadata: Metadata of the model version, defaults to None
+:type metadata: List[Dict[str, str]], optional
+:param cost: Cost of the model version, defaults to None
+:type cost: List[Dict[str, str]], optional
+:param suggested_replacements: Suggested replacements for the model version, defaults to None
+:type suggested_replacements: List[str], optional
+:param streaming_supported: True if streaming is supported, otherwise false
+:type streaming_supported: bool, optional
+:param orchestration_capabilities: Orchestration capabilities of the model version, defaults to None
+:type orchestration_capabilities: List[str], optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + is_latest: bool, + deprecated: bool, + retirement_date: Optional[datetime.datetime] = None, + context_length: Optional[int] = None, + input_types: Optional[List[str]] = None, + capabilities: Optional[List[str]] = None, + metadata: Optional[Dict[str, str]] = None, + cost: Optional[Dict[str, str]] = None, + suggested_replacements: Optional[List[str]] = None, + streaming_supported: Optional[bool] = None, + orchestration_capabilities: Optional[List[str]] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(model_version_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.model_version.ModelVersionobject, created from the values in the dict
+provided as parameter

+:param model_version_dict: Dict which includes the necessary values to create the object
+:type model_version_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.model_version.ModelVersion`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+Optional = typing.Optional
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.output_artifact.html b/packages/base/docs/ai_api_client_sdk.models.output_artifact.html new file mode 100644 index 0000000..1c2b58d --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.output_artifact.html @@ -0,0 +1,93 @@ + + + + +Python: module ai_api_client_sdk.models.output_artifact + + + + + +
 
ai_api_client_sdk.models.output_artifact
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/output_artifact.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
OutputArtifact +
+
+
+

+ + + + + + + +
 
class OutputArtifact(builtins.object)
   OutputArtifact(
+    name: str,
+    kind: str = None,
+    description: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None
+)

+The OutputArtifact object defines the output artifact specified in the executable definition.
+:param name: name of artifact
+:type name: str
+:param kind: kind of artifact (Dataset, Model, ResultSet)
+:type kind: str
+:param description: description of artifact
+:type description: str
+:param labels: labels for artifact
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + kind: str = None, + description: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(output_artifact_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.output_artifact.OutputArtifactobject, created from the values
+in the dict provided as parameter

+:param output_artifact_dict: Dict which includes the necessary values to create the object
+:type output_artifact_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.output_artifact.OutputArtifact`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.parameter.html b/packages/base/docs/ai_api_client_sdk.models.parameter.html new file mode 100644 index 0000000..2c23ff3 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.parameter.html @@ -0,0 +1,98 @@ + + + + +Python: module ai_api_client_sdk.models.parameter + + + + + +
 
ai_api_client_sdk.models.parameter
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/parameter.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Parameter +
+
+
+

+ + + + + + + +
 
class Parameter(builtins.object)
   Parameter(
+    name: str,
+    type: ai_api_client_sdk.models.parameter.Parameter.Type,
+    description: str = None,
+    default: str = None,
+    **kwargs
+)

+The Parameter object defines the parameter specified in the executable definition.

+:param name: name of the parameter
+:type name: str
+:param type: Type of the parameter
+:type type: class:`ai_api_client_sdk.models.parameter.Parameter.Type`
+:param description: description for parameter
+:type description: str
+:param default: default value for parameter
+:type default: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: str, + type: ai_api_client_sdk.models.parameter.Parameter.Type, + description: str = None, + default: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(parameter_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.parameter.Parameterobject, created from the values in the dict
+provided as parameter

+:param parameter_dict: Dict which includes the necessary values to create the object
+:type parameter_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.parameter.Parameter`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
Type = <enum 'Type'>
+ +
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.parameter_binding.html b/packages/base/docs/ai_api_client_sdk.models.parameter_binding.html new file mode 100644 index 0000000..e9ac637 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.parameter_binding.html @@ -0,0 +1,84 @@ + + + + +Python: module ai_api_client_sdk.models.parameter_binding + + + + + +
 
ai_api_client_sdk.models.parameter_binding
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/parameter_binding.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.KeyValue(builtins.object) +
+
+
ParameterBinding +
+
+
+

+ + + + + + + +
 
class ParameterBinding(ai_api_client_sdk.models.base_models.KeyValue)
   ParameterBinding(key: str, value: str, **kwargs)

+The ParameterBinding object defines the input artifact specified in the configuration, as a key-value pair. Refer
+to :class:`ai_api_client_sdk.models.base_models.KeyValue`, for the object definition
 
 
Method resolution order:
+
ParameterBinding
+
ai_api_client_sdk.models.base_models.KeyValue
+
builtins.object
+
+
+Methods defined here:
+
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(parameter_binding_dict: Dict[str, str])
Returns a :class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding` object, created from
+the values in the dict provided as parameter

+:param parameter_binding_dict: Dict which includes the necessary values to create the object
+:type parameter_binding_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, key: str, value: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
to_dict(self)
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.KeyValue:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.resource_group.html b/packages/base/docs/ai_api_client_sdk.models.resource_group.html new file mode 100644 index 0000000..6f38a0d --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.resource_group.html @@ -0,0 +1,92 @@ + + + + +Python: module ai_api_client_sdk.models.resource_group + + + + + +
 
ai_api_client_sdk.models.resource_group
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/resource_group.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ResourceGroup +
+
+
+

+ + + + + + + +
 
class ResourceGroup(builtins.object)
   ResourceGroup(
+    resource_group_id: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None,
+    status: ai_api_client_sdk.models.resource_group_status.ResourceGroupStatus = None,
+    created_at: datetime.datetime = None,
+    *args,
+    **kwargs
+)

+ResourceGroup represents the resource group.

+:param resource_group_id: The resource_group_id of this ResourceGroup.
+:type resource_group_id: str
+:param labels: The labels of this ResourceGroup.
+:type labels: ResourceGroupLabels
+:param status: The status of this ResourceGroup.
+:type status: str
+:param created_at: Time when the resource group was created
+:type created_at: datetime
 
 Methods defined here:
+
__init__( + self, + resource_group_id: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + status: ai_api_client_sdk.models.resource_group_status.ResourceGroupStatus = None, + created_at: datetime.datetime = None, + *args, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(resource_group_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.resource_group.ResourceGroupobject, created
+from the values in the dict provided as parameter

+:param resource_group_dict: Dict which includes the necessary values to create the object
+:type resource_group_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.resource_group_query_response.html b/packages/base/docs/ai_api_client_sdk.models.resource_group_query_response.html new file mode 100644 index 0000000..9f1a4fd --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.resource_group_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_api_client_sdk.models.resource_group_query_response + + + + + +
 
ai_api_client_sdk.models.resource_group_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/resource_group_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ResourceGroupQueryResponse +
+
+
+

+ + + + + + + +
 
class ResourceGroupQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ResourceGroupQueryResponse(
+    resources: List[ai_api_client_sdk.models.resource_group.ResourceGroup],
+    count: int,
+    **kwargs
+)

+The ResourceGroupQueryResponse object defines the response of the resourceGroups query request
+:param resources: List of the resource groups returned from the server
+:type resources: List[class:`ai_core_sdk.models.resource_group.ResourceGroup`]
+:param count: Total number of the queried docker registry secrets
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ResourceGroupQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.resource_group.ResourceGroup], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.resource_group_status.html b/packages/base/docs/ai_api_client_sdk.models.resource_group_status.html new file mode 100644 index 0000000..0918241 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.resource_group_status.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_api_client_sdk.models.resource_group_status + + + + + +
 
ai_api_client_sdk.models.resource_group_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/resource_group_status.py
+

+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
ResourceGroupStatus +
+
+
+

+ + + + + + + +
 
class ResourceGroupStatus(enum.Enum)
   ResourceGroupStatus(*values)

+ResourceGroupStatus is an Enum defining the valid values of the status of a resource group
 
 
Method resolution order:
+
ResourceGroupStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
ERROR = <ResourceGroupStatus.ERROR: 'ERROR'>
+ +
PROVISIONED = <ResourceGroupStatus.PROVISIONED: 'PROVISIONED'>
+ +
PROVISIONING = <ResourceGroupStatus.PROVISIONING: 'PROVISIONING'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.scenario.html b/packages/base/docs/ai_api_client_sdk.models.scenario.html new file mode 100644 index 0000000..db28c48 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.scenario.html @@ -0,0 +1,109 @@ + + + + +Python: module ai_api_client_sdk.models.scenario + + + + + +
 
ai_api_client_sdk.models.scenario
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/scenario.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Scenario +
+
+
+

+ + + + + + + +
 
class Scenario(builtins.object)
   Scenario(
+    id: str,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    name: str,
+    description: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None,
+    **kwargs
+)

+The Scenario object defines a scenario
+:param id: ID of the scenario
+:type id: str
+:param created_at: Time when the scenario was created
+:type created_at: datetime
+:param modified_at: Time when the scenario was last modified
+:type modified_at: datetime
+:param name: Name of the scenario
+:type name: str
+:param description: Description of the scenario, defaults to None
+:type description: str, optional
+:param labels: List of the labels of the scenario, defaults to None
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + id: str, + created_at: datetime.datetime, + modified_at: datetime.datetime, + name: str, + description: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
is_llm_scenario(self)
Returns if the scenario is a LLM scenario or not

+:return: True if scenario is llm scenario, False otherwise
+:rtype: bool
+ +
+Static methods defined here:
+
from_dict(scenario_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.scenario.Scenarioobject, created from the values in the dict
+provided as parameter

+:param scenario_dict: Dict which includes the necessary values to create the object
+:type scenario_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.scenario.Scenario`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.scenario_query_response.html b/packages/base/docs/ai_api_client_sdk.models.scenario_query_response.html new file mode 100644 index 0000000..9519075 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.scenario_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.scenario_query_response + + + + + +
 
ai_api_client_sdk.models.scenario_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/scenario_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ScenarioQueryResponse +
+
+
+

+ + + + + + + +
 
class ScenarioQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ScenarioQueryResponse(
+    resources: List[ai_api_client_sdk.models.scenario.Scenario],
+    count: int,
+    **kwargs
+)

+The ScenarioQueryResponse object defines the response of the scenario query request
+:param resources: List of the scenarios returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.scenario.Scenario`]
+:param count: Total number of the queried scenarios
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ScenarioQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.scenario.Scenario], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.status.html b/packages/base/docs/ai_api_client_sdk.models.status.html new file mode 100644 index 0000000..190080c --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.status.html @@ -0,0 +1,143 @@ + + + + +Python: module ai_api_client_sdk.models.status + + + + + +
 
ai_api_client_sdk.models.status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/status.py
+

+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
ScheduleStatus +
Status +
+
+
+

+ + + + + + + +
 
class ScheduleStatus(enum.Enum)
   ScheduleStatus(*values)

+Enum defining the status values for execution schedules
 
 
Method resolution order:
+
ScheduleStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
ACTIVE = <ScheduleStatus.ACTIVE: 'ACTIVE'>
+ +
INACTIVE = <ScheduleStatus.INACTIVE: 'INACTIVE'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ + + + + + + +
 
class Status(enum.Enum)
   Status(*values)

+Status is an Enum defining the valid values of the status of an execution/deployment
 
 
Method resolution order:
+
Status
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
COMPLETED = <Status.COMPLETED: 'COMPLETED'>
+ +
DEAD = <Status.DEAD: 'DEAD'>
+ +
PENDING = <Status.PENDING: 'PENDING'>
+ +
RUNNING = <Status.RUNNING: 'RUNNING'>
+ +
STOPPED = <Status.STOPPED: 'STOPPED'>
+ +
STOPPING = <Status.STOPPING: 'STOPPING'>
+ +
UNKNOWN = <Status.UNKNOWN: 'UNKNOWN'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.target_status.html b/packages/base/docs/ai_api_client_sdk.models.target_status.html new file mode 100644 index 0000000..52ed398 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.target_status.html @@ -0,0 +1,84 @@ + + + + +Python: module ai_api_client_sdk.models.target_status + + + + + +
 
ai_api_client_sdk.models.target_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/target_status.py
+

+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
TargetStatus +
+
+
+

+ + + + + + + +
 
class TargetStatus(enum.Enum)
   TargetStatus(*values)

+TargetStatus is an Enum defining the valid values of the target status of an execution/deployment
 
 
Method resolution order:
+
TargetStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
COMPLETED = <TargetStatus.COMPLETED: 'COMPLETED'>
+ +
DELETED = <TargetStatus.DELETED: 'DELETED'>
+ +
RUNNING = <TargetStatus.RUNNING: 'RUNNING'>
+ +
STOPPED = <TargetStatus.STOPPED: 'STOPPED'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.version.html b/packages/base/docs/ai_api_client_sdk.models.version.html new file mode 100644 index 0000000..01c5c0c --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.version.html @@ -0,0 +1,93 @@ + + + + +Python: module ai_api_client_sdk.models.version + + + + + +
 
ai_api_client_sdk.models.version
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/version.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Version +
+
+
+

+ + + + + + + +
 
class Version(builtins.object)
   Version(
+    id: str,
+    scenario_id: str,
+    created_at: datetime.datetime,
+    modified_at: datetime.datetime,
+    description: str = None,
+    **kwargs
+)

+The Version object defines a scenario
+:param id: ID of the version
+:type id: str
+:param scenario_id: ID of the scenario the version belongs to
+:type scenario_id: str
+:param created_at: Time when the scenario was created
+:type created_at: datetime
+:param modified_at: Time when the scenario was last modified
+:type modified_at: datetime
+:param description: Description of the scenario, defaults to None
+:type description: str, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + id: str, + scenario_id: str, + created_at: datetime.datetime, + modified_at: datetime.datetime, + description: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(version_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.version.Versionobject, created from the values in the dict
+provided as parameter

+:param version_dict: Dict which includes the necessary values to create the object
+:type version_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.version.Version`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.version_list.html b/packages/base/docs/ai_api_client_sdk.models.version_list.html new file mode 100644 index 0000000..d3a6baa --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.version_list.html @@ -0,0 +1,77 @@ + + + + +Python: module ai_api_client_sdk.models.version_list + + + + + +
 
ai_api_client_sdk.models.version_list
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/version_list.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
VersionList +
+
+
+

+ + + + + + + +
 
class VersionList(builtins.object)
   VersionList(
+    versions: List[ai_api_client_sdk.models.api_version.APIVersion] = None
+)

+The VersionList object, is a list of API version descriptions

+:param versions: A list of objects describing the API versions, defaults to None
+:type versions: class:`ai_api_client_sdk.models.api_version.APIVersion`, optional
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__( + self, + versions: List[ai_api_client_sdk.models.api_version.APIVersion] = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(version_list_dict: Dict[str, List[Dict[str, str]]])
Returns a :class:`ai_api_client_sdk.models.version_list.VersionListobject, created from the
+values in the dict provided as parameter

+:param version_list_dict: Dict which includes the necessary values to create the object
+:type version_list_dict: Dict[str, List[Dict[str, str]]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.version_list.VersionList`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.models.version_query_response.html b/packages/base/docs/ai_api_client_sdk.models.version_query_response.html new file mode 100644 index 0000000..c8cca65 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.models.version_query_response.html @@ -0,0 +1,90 @@ + + + + +Python: module ai_api_client_sdk.models.version_query_response + + + + + +
 
ai_api_client_sdk.models.version_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/models/version_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
VersionQueryResponse +
+
+
+

+ + + + + + + +
 
class VersionQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   VersionQueryResponse(
+    resources: List[ai_api_client_sdk.models.version.Version],
+    count: int,
+    **kwargs
+)

+The VersionQueryResponse object defines the response of the version query request
+:param resources: List of the versions returned from the server
+:type resources: List[class:`ai_api_client_sdk.models.version.Version`]
+:param count: Total number of the queried versions
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
VersionQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_api_client_sdk.models.version.Version], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a :class:`ai_api_client_sdk.models.version_query_response.VersionQueryResponse` object, created
+from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_api_client_sdk.models.version_query_response.VersionQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.artifact_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.artifact_client.html new file mode 100644 index 0000000..20ac44b --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.artifact_client.html @@ -0,0 +1,212 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.artifact_client + + + + + +
 
ai_api_client_sdk.resource_clients.artifact_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/artifact_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ArtifactClient +
+
+
+

+ + + + + + + +
 
class ArtifactClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ArtifactClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+ArtifactClient is a class implemented for interacting with the artifact related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ArtifactClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
count( + self, + scenario_id: str = None, + execution_id: str = None, + name: str = None, + kind: ai_api_client_sdk.models.artifact.Artifact.Kind = None, + artifact_label_selector: List[str] = None, + resource_group: str = None +) -> int
Counts the artifacts.

+:param scenario_id: ID of the scenario the artifacts should belong to, defaults to None
+:type scenario_id: str, optional
+:param execution_id: ID of the execution the artifact should be resulted from, defaults to None
+:type execution_id: str, optional
+:param name: Name of the artifact(s) to be retrieved, defaults to None
+:type name: str, optional
+:param kind: Kind of the artifacts to be retrieved, defaults to None
+:type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`, optional
+:param artifact_label_selector: list of the label selector strings in the form of "key=value" or "key!=value", to filter
+    the artifacts with respect to their labels, defaults to None
+:type artifact_label_selector: List[str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: int
+ +
create( + self, + name: str, + kind: ai_api_client_sdk.models.artifact.Artifact.Kind, + url: str, + scenario_id: str, + description: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse
Creates an artifact.

+:param name: Name of the artifact
+:type name: str
+:param kind: Kind of the artifact
+:type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`
+:param url: URL of the artifact
+:type url: str
+:param scenario_id: ID of the scenario which the artifact should belong to
+:type scenario_id: str
+:param description: Description of the artifact, defaults to None
+:type description: str, optional
+:param labels: List of the labels of the artifact, defaults to None
+:type labels: List[class:`ai_api_client_sdk.models.label.Label`]
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.artifact_create_response.ArtifactCreateResponse`
+ +
get(self, artifact_id: str, expand: str = None, resource_group: str = None) -> ai_api_client_sdk.models.artifact.Artifact
Retrieves the artifact from the server.

+:param artifact_id: ID of the artifact to be retrieved
+:type artifact_id: str
+:param expand: Entity whose details to be displayed in the response, defaults to None
+:type expand: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved artifact
+:rtype: class:`ai_api_client_sdk.models.artifact.Artifact`
+ +
query( + self, + scenario_id: str = None, + execution_id: str = None, + name: str = None, + kind: ai_api_client_sdk.models.artifact.Artifact.Kind = None, + artifact_label_selector: List[str] = None, + top: int = None, + skip: int = None, + search: str = None, + search_case_insensitive: bool = None, + expand: str = None, + resource_group: str = None +) -> ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse
Queries the artifacts.

+:param scenario_id: ID of the scenario the artifacts should belong to, defaults to None
+:type scenario_id: str, optional
+:param execution_id: ID of the execution the artifact should be resulted from, defaults to None
+:type execution_id: str, optional
+:param name: Name of the artifact(s) to be retrieved, defaults to None
+:type name: str, optional
+:param kind: Kind of the artifacts to be retrieved, defaults to None
+:type kind: class:`ai_api_client_sdk.models.artifact.Artifact.Kind`, optional
+:param artifact_label_selector: Query the artifacts based on their labels in the form of "key=value" or
+    "key!=value" separated by commas, defaults to None
+:type artifact_label_selector: list, optional
+:param top: Number of artifacts to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of artifacts to be skipped, from the list of the queried artifacts, defaults to None
+:type skip: int, optional
+:param search: Generic search term to be looked for in various attributes of artifacts, defaults to None
+:type search: str, optional
+:param search_case_insensitive: Indicates whether the search should be case insensitive
+:type search_case_insensitive: bool, optional
+:param expand: Entity whose details to be displayed in the response, defaults to None
+:type expand: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.artifact_query_response.ArtifactQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.base_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.base_client.html new file mode 100644 index 0000000..c1400f0 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.base_client.html @@ -0,0 +1,75 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.base_client + + + + + +
 
ai_api_client_sdk.resource_clients.base_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/base_client.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
BaseClient +
+
+
+

+ + + + + + + +
 
class BaseClient(builtins.object)
   BaseClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+BaseClient defines the interface for the resource clients.

+:param rest_client: the client used to make calls to the server
+:type rest_client: class:`ai_api_client_sdk.helpers.rest_client.RestClient`
 
 Methods defined here:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query(self, *args, **kwargs)
Queries the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.configuration_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.configuration_client.html new file mode 100644 index 0000000..c5f5fe1 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.configuration_client.html @@ -0,0 +1,197 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.configuration_client + + + + + +
 
ai_api_client_sdk.resource_clients.configuration_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/configuration_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ConfigurationClient +
+
+
+

+ + + + + + + +
 
class ConfigurationClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ConfigurationClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+ConfigurationClient is a class implemented for interacting with the configuration related endpoints of the
+server. It implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ConfigurationClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
count( + self, + scenario_id: str = None, + executable_ids: List[str] = None, + search: str = None, + resource_group: str = None +) -> int
Counts the configurations.

+:param scenario_id: ID of the scenario the configurations should belong to, defaults to None
+:type scenario_id: str, optional
+:param executable_ids: IDs of the executables the configurations should have configured, defaults to None
+:type executable_ids: List[str], optional
+:param search: Generic search term to be looked for in various attributes of configurations, defaults to None
+:type search: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: int
+ +
create( + self, + name: str, + scenario_id: str, + executable_id: str, + parameter_bindings: List[ai_api_client_sdk.models.parameter_binding.ParameterBinding] = None, + input_artifact_bindings: List[ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse
Creates a configuration.

+:param name: Name of the configuration
+:type name: str
+:param scenario_id: ID of the scenario which the configuration should belong to
+:type scenario_id: str
+:param executable_id: ID of the executable, which should be configured
+:type executable_id: str
+:param parameter_bindings: List of the input parameters, defaults to None
+:type parameter_bindings: List[class:`ai_api_client_sdk.models.parameter_binding.ParameterBinding`], optional
+:param input_artifact_bindings: List of the input artifacts which are to be used by the executable,
+    defaults to None
+:type input_artifact_bindings:
+    List[class:`ai_api_client_sdk.models.input_artifact_binding.InputArtifactBinding`], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.configuration_create_response.ConfigurationCreateResponse`
+ +
get(self, configuration_id: str, expand: str = None, resource_group: str = None) -> ai_api_client_sdk.models.configuration.Configuration
Retrieves the configuration from the server.

+:param configuration_id: ID of the configuration to be retrieved
+:type configuration_id: str
+:param expand: Entity whose details to be displayed in the response, defaults to None
+:type expand: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved configuration
+:rtype: class:`ai_api_client_sdk.models.configuration.Configuration`
+ +
query( + self, + scenario_id: str = None, + executable_ids: List[str] = None, + top: int = None, + skip: int = None, + search: str = None, + search_case_insensitive: bool = None, + expand: str = None, + resource_group: str = None +) -> ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse
Queries the configurations.

+:param scenario_id: ID of the scenario the configurations should belong to, defaults to None
+:type scenario_id: str, optional
+:param executable_ids: IDs of the executables the configurations should have configured, defaults to None
+:type executable_ids: List[str], optional
+:param top: Number of configurations to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of configurations to be skipped, from the list of the queried configurations, defaults to
+    None
+:type skip: int, optional
+:param search: Generic search term to be looked for in various attributes of configurations, defaults to None
+:type search: str, optional
+:param search_case_insensitive: Indicates whether the search should be case insensitive
+:type search_case_insensitive: bool, optional
+:param expand: Entity whose details to be displayed in the response, defaults to None
+:type expand: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.configuration_query_response.ConfigurationQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.deployment_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.deployment_client.html new file mode 100644 index 0000000..92277d8 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.deployment_client.html @@ -0,0 +1,280 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.deployment_client + + + + + +
 
ai_api_client_sdk.resource_clients.deployment_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/deployment_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
DeploymentClient +
+
+
+

+ + + + + + + +
 
class DeploymentClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   DeploymentClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+DeploymentClient is a class implemented for interacting with the deployment related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
DeploymentClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
bulk_modify( + self, + deployments: List[ai_api_client_sdk.models.base_models.BasicModifyRequest], + resource_group: str = None +) -> ai_api_client_sdk.models.deployment_bulk_modify_response.DeploymentBulkModifyResponse
Modifies the deployments
+:param deployments: List of deployment modify requests
+:type deployments: List[DeploymentModifyRequest]
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.deployment_bulk_modify_response.DeploymentBulkModifyResponse`
+ +
count( + self, + scenario_id: str = None, + configuration_id: str = None, + executable_ids: List[str] = None, + status: ai_api_client_sdk.models.status.Status = None, + resource_group: str = None +) -> int
Counts the number of deployments.

+:param scenario_id: ID of the scenario, the deployments should belong to, defaults to None
+:type scenario_id: str, optional
+:param configuration_id: ID of the configuration, the deployments should be configured by, defaults to None
+:type configuration_id: str, optional
+:param executable_ids: IDs of the executables, the deployments should be created from, defaults to None
+:type executable_ids: List[str], optional
+:param status: Status which the deployments should currently have
+:type status: class:`ai_api_client_sdk.models.status.Status`, optional
+:param resource_group: Resource group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: int
+ +
create(self, configuration_id: str, ttl: str = None, resource_group: str = None) -> ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse
Creates a deployment.

+:param configuration_id: ID of the configuration, that should configure the deployment
+:type configuration_id: str
+:param ttl: Time to live for deployment and can be none or take  a number followed by the unit
+(any of following values, minutes(m|M), hours(h|H) or days(d|D))
+:type ttl: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.deployment_create_response.DeploymentCreateResponse`
+ +
delete(self, deployment_id: str, resource_group: str = None) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the deployment.

+:param deployment_id: ID of the deployment to be deleted
+:type deployment_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, deployment_id: str, resource_group: str = None, select: str = None) -> Union[ai_api_client_sdk.models.deployment.Deployment, ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse]
Retrieves the deployment from the server.

+:param deployment_id: ID of the deployment to be retrieved
+:type deployment_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param select: only status supported. Get deployment for a given deployment id and select status
+:type select: str, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved deployment
+:rtype: class:Union[`ai_api_client_sdk.models.deployment.Deployment`,
+    `ai_api_client_sdk.models.deployment_get_status_response.DeploymentGetStatusResponse`]
+ +
modify( + self, + deployment_id: str, + target_status: ai_api_client_sdk.models.target_status.TargetStatus = None, + configuration_id: str = None, + resource_group: str = None +) -> ai_api_client_sdk.models.base_models.BasicResponse
Modifies the deployment, by changing either the target status, or the configuration ID.

+:param deployment_id: ID of the deployment to be modified
+:type deployment_id: str
+:param target_status: Desired target status of the deployment, defaults to None
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`, optional
+:param configuration_id: ID of the new configuration to be used by the deployment, defaults to None
+:type configuration_id: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query( + self, + scenario_id: str = None, + configuration_id: str = None, + executable_ids: List[str] = None, + status: ai_api_client_sdk.models.status.Status = None, + top: int = None, + skip: int = None, + resource_group: str = None +) -> ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse
Queries the deployments.

+:param scenario_id: ID of the scenario the deployments should belong to, defaults to None
+:type scenario_id: str, optional
+:param configuration_id: ID of the configuration, the deployments should be configured by, defaults to None
+:type configuration_id: str, optional
+:param executable_ids: IDs of the executables the deployments should be created from, defaults to None
+:type executable_ids: List[str], optional
+:param status: Status which the deployments should currently have
+:type status: class:`ai_api_client_sdk.models.status.Status`, optional
+:param top: Number of deployments to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of deployments to be skipped, from the list of the queried deployments, defaults to None
+:type skip: int, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.deployment_query_response.DeploymentQueryResponse`
+ +
query_logs( + self, + deployment_id: str, + top: int = None, + start: datetime.datetime = None, + end: datetime.datetime = None, + order: ai_api_client_sdk.models.base_models.Order = None, + resource_group: str = None +) -> ai_api_client_sdk.models.log_response.LogResponse
Queries the logs of the deployment.

+:param deployment_id: ID of the deployment
+:type deployment_id: str
+:param top: The max number of entries to return. Defaults to 1000. Limited to 5000 max.
+:type top: int
+:param start: The start time for the query. Defaults to one hour ago.
+:type start: datetime
+:param end: The end time for the query. Defaults to now.
+:type end: datetime
+:param order: Determines the sort order with respect to time. Defaults to asc.
+:type order: class:`ai_api_client_sdk.models.base_models.Order`
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: Logs from the execution
+:rtype: class:`ai_api_client_sdk.models.log_response.LogResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.executable_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.executable_client.html new file mode 100644 index 0000000..2f6e1fb --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.executable_client.html @@ -0,0 +1,115 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.executable_client + + + + + +
 
ai_api_client_sdk.resource_clients.executable_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/executable_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ExecutableClient +
+
+
+

+ + + + + + + +
 
class ExecutableClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ExecutableClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+ExecutableClient is a class implemented for interacting with the executable related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ExecutableClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
get(self, scenario_id: str, executable_id: str, resource_group: str = None) -> ai_api_client_sdk.models.executable.Executable
Retrieves the executable from the server.

+:param scenario_id: ID of the scenario the executable belongs to
+:type scenario_id: str
+:param executable_id: ID of the executable to be retrieved
+:type executable_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved executable
+:rtype: class:`ai_api_client_sdk.models.executable.Executable`
+ +
query( + self, + scenario_id: str, + version_id: str = None, + resource_group: str = None +) -> ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse
Queries the executables.

+:param scenario_id: ID of the scenario the executables should belong to, defaults to None
+:type scenario_id: str, optional
+:param version_id: ID of the version, the executions should have, defaults to None
+:type version_id: str, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.executable_query_response.ExecutableQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.execution_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.execution_client.html new file mode 100644 index 0000000..d208296 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.execution_client.html @@ -0,0 +1,282 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.execution_client + + + + + +
 
ai_api_client_sdk.resource_clients.execution_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/execution_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ExecutionClient +
+
+
+

+ + + + + + + +
 
class ExecutionClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ExecutionClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+ExecutionClient is a class implemented for interacting with the execution related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ExecutionClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
bulk_modify( + self, + executions: List[ai_api_client_sdk.models.base_models.BasicModifyRequest], + resource_group: str = None +) -> ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse
Modifies the executions
+:param executions: List of execution modify requests
+:type executions: List[ExecutionModifyRequest]
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.execution_bulk_modify_response.ExecutionBulkModifyResponse`
+ +
count( + self, + scenario_id: str = None, + configuration_id: str = None, + executable_ids: List[str] = None, + execution_schedule_id: str = None, + status: ai_api_client_sdk.models.status.Status = None, + resource_group: str = None +) -> int
Counts the number of executions.

+:param scenario_id: ID of the scenario, the executions should belong to, defaults to None
+:type scenario_id: str, optional
+:param configuration_id: ID of the configuration, the executions should be configured by, defaults to None
+:type configuration_id: str, optional
+:param executable_ids: IDs of the executables, the executions should be created from, defaults to None
+:type executable_ids: List[str], optional
+:param execution_schedule_id: ID of the execution schedule, defaults to None
+:type execution_schedule_id: str, optional
+:param status: Status which the executions should currently have
+:type status: class:`ai_api_client_sdk.models.status.Status`, optional
+:param resource_group: Resource group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: int
+ +
create(self, configuration_id: str, resource_group: str = None) -> ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse
Creates an execution.

+:param configuration_id: ID of the configuration, that should configure the execution
+:type configuration_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.execution_create_response.ExecutionCreateResponse`
+ +
delete(self, execution_id: str, resource_group: str = None) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the execution.

+:param execution_id: ID of the execution to be deleted
+:type execution_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, execution_id: str, resource_group: str = None, select: str = None) -> Union[ai_api_client_sdk.models.execution.Execution, ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse]
Retrieves the execution from the server.

+:param execution_id: ID of the execution to be retrieved
+:type execution_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param select: only status supported. Get execution for a given execution id and select status
+:type select: str, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved execution
+:rtype: class:Union[`ai_api_client_sdk.models.execution.Execution`,
+    `ai_api_client_sdk.models.execution_get_status_response.ExecutionGetStatusResponse`]
+ +
modify( + self, + execution_id: str, + target_status: ai_api_client_sdk.models.target_status.TargetStatus, + resource_group: str = None +) -> ai_api_client_sdk.models.base_models.BasicResponse
Modifies the execution, by changing the target status.

+:param execution_id: ID of the execution to be modified
+:type execution_id: str
+:param target_status: Desired target status of the execution, defaults to None
+:type target_status: class:`ai_api_client_sdk.models.target_status.TargetStatus`, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query( + self, + scenario_id: str = None, + configuration_id: str = None, + executable_ids: List[str] = None, + execution_schedule_id: str = None, + status: ai_api_client_sdk.models.status.Status = None, + top: int = None, + skip: int = None, + resource_group: str = None +) -> ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse
Queries the executions.

+:param scenario_id: ID of the scenario the executions should belong to, defaults to None
+:type scenario_id: str, optional
+:param configuration_id: ID of the configuration, the executions should be configured by, defaults to None
+:type configuration_id: str, optional
+:param executable_ids: IDs of the executables the executions should be created from, defaults to None
+:type executable_ids: List[str], optional
+:param execution_schedule_id: ID of the execution schedule, defaults to None
+:type execution_schedule_id: str, optional
+:param status: Status which the executions should currently have
+:type status: class:`ai_api_client_sdk.models.status.Status`, optional
+:param top: Number of executions to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of executions to be skipped, from the list of the queried executions, defaults to None
+:type skip: int, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse`
+ +
query_logs( + self, + execution_id: str, + top: int = None, + start: datetime.datetime = None, + end: datetime.datetime = None, + order: ai_api_client_sdk.models.base_models.Order = None, + resource_group: str = None +) -> ai_api_client_sdk.models.log_response.LogResponse
Queries the logs of the execution.

+:param execution_id: ID of the execution
+:type execution_id: str
+:param top: The max number of entries to return. Defaults to 1000. Limited to 5000 max.
+:type top: int
+:param start: The start time for the query. Defaults to one hour ago.
+:type start: datetime
+:param end: The end time for the query. Defaults to now.
+:type end: datetime
+:param order: Determines the sort order with respect to time. Defaults to asc.
+:type order: class:`ai_api_client_sdk.models.base_models.Order`
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: Logs from the execution
+:rtype: class:`ai_api_client_sdk.models.log_response.LogResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.execution_schedule_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.execution_schedule_client.html new file mode 100644 index 0000000..54c1b49 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.execution_schedule_client.html @@ -0,0 +1,235 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.execution_schedule_client + + + + + +
 
ai_api_client_sdk.resource_clients.execution_schedule_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/execution_schedule_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ExecutionScheduleClient +
+
+
+

+ + + + + + + +
 
class ExecutionScheduleClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ExecutionScheduleClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+ExecutionScheduleClient is a class implemented for interacting with the execution schedules related endpoints of
+the server. It implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ExecutionScheduleClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
count( + self, + configuration_id: str = None, + status: ai_api_client_sdk.models.status.ScheduleStatus = None, + resource_group: str = None +) -> int
Counts the number of executions schedules.

+:param configuration_id: ID of the configuration, the executions should be configured by, defaults to None
+:type configuration_id: str, optional
+:param status: ScheduleStatus which the execution schedule should currently have, defaults to None
+:type status: class:`ai_api_client_sdk.models.schedule_status.ScheduleStatus`, optional
+:param resource_group: Resource group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: int
+ +
create( + self, + name: str, + cron: str, + configuration_id: str, + start: datetime.datetime = None, + end: datetime.datetime = None, + resource_group: str = None +) -> ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse
Creates an execution schedule.

+:param name: Name of the execution schedule
+:type name: str
+:param cron: Cron defining the schedule to run the executions
+:type name: str
+:param configuration_id: ID of the configuration for the execution schedule
+:type configuration_id: str
+:param start: Start time of the execution schedule in UTC, defaults to None
+:type start: datetime, optional
+:param end: End time of the execution schedule in UTC e.g., defaults to None
+:type end: datetime, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.execution_schedule_create_response.ExecutionScheduleCreateResponse`
+ +
delete(self, execution_schedule_id: str, resource_group: str = None) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the execution schedule.

+:param execution_schedule_id: ID of the execution schedule to be deleted
+:type execution_schedule_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, execution_schedule_id: str, resource_group: str = None) -> ai_api_client_sdk.models.execution_schedule.ExecutionSchedule
Retrieves the execution schedule from the server.

+:param execution_schedule_id: ID of the execution schedule to be retrieved
+:type execution_schedule_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved execution
+:rtype: class:`ai_api_client_sdk.models.execution.Execution`
+ +
modify( + self, + execution_schedule_id: str, + cron: str = None, + start: datetime.datetime = None, + end: datetime.datetime = None, + configurationId: str = None, + status: ai_api_client_sdk.models.status.ScheduleStatus = None, + resource_group: str = None +) -> ai_api_client_sdk.models.base_models.BasicResponse
Modifies the execution schedule.

+:param execution_schedule_id: ID of the execution to be modified
+:type execution_schedule_id: str
+:param cron: Cron defining the schedule to run the executions, defaults to None
+:type cron: str, optional
+:param configurationId: ID of the configuration for the execution schedule, defaults to None
+:type configurationId: str, optional
+:param start: Start time of the execution schedule in UTC, defaults to None
+:type start: datetime, optional
+:param end: End time of the execution schedule in UTC, defaults to None
+:type end: datetime, optional
+:param status: pause / resume Status of the execution schedule, defaults to None
+:type status: class:`ai_api_client_sdk.models.status.ScheduleStatus`, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query( + self, + configuration_id: str = None, + status: ai_api_client_sdk.models.status.ScheduleStatus = None, + top: int = None, + skip: int = None, + resource_group: str = None +) -> ai_api_client_sdk.models.execution_schedule_query_response.ExecutionScheduleQueryResponse
Queries the execution schedules.

+:param configuration_id: ID of the configuration, the executions should be configured by, defaults to None
+:type configuration_id: str, optional
+:param status:  ScheduleStatus which the execution schedule should currently have
+:type status: class:`ai_api_client_sdk.models.schedule_status.ScheduleStatus`, optional
+:param top: Number of executions to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of executions to be skipped, from the list of the queried executions, defaults to None
+:type skip: int, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.execution_query_response.ExecutionQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.healthz_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.healthz_client.html new file mode 100644 index 0000000..613facd --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.healthz_client.html @@ -0,0 +1,79 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.healthz_client + + + + + +
 
ai_api_client_sdk.resource_clients.healthz_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/healthz_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
HealthzClient +
+
+
+

+ + + + + + + +
 
class HealthzClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   HealthzClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+HealthzClient is a class implemented for interacting with the healthz endpoint of the server. It implements the
+base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
HealthzClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
get(self) -> ai_api_client_sdk.models.healthz_status.HealthzStatus
Retrieves the health status of the server.

+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The health status of the server
+:rtype: class:`ai_api_client_sdk.models.healthz_status.HealthzStatus`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query(self, *args, **kwargs)
Queries the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.html b/packages/base/docs/ai_api_client_sdk.resource_clients.html new file mode 100644 index 0000000..8790b97 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.html @@ -0,0 +1,33 @@ + + + + +Python: package ai_api_client_sdk.resource_clients + + + + + +
 
ai_api_client_sdk.resource_clients
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/__init__.py
+

+

+ + + + + +
 
Package Contents
       
artifact_client
+base_client
+configuration_client
+deployment_client
+
executable_client
+execution_client
+execution_schedule_client
+healthz_client
+
meta_client
+metrics_client
+model_client
+resource_groups_client
+
scenario_client
+
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.meta_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.meta_client.html new file mode 100644 index 0000000..40eb4a2 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.meta_client.html @@ -0,0 +1,75 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.meta_client + + + + + +
 
ai_api_client_sdk.resource_clients.meta_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/meta_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
MetaClient +
+
+
+

+ + + + + + + +
 
class MetaClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   MetaClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+
 
 
Method resolution order:
+
MetaClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
get(self) -> ai_api_client_sdk.models.capabilities.Capabilities
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
get_versions(self) -> ai_api_client_sdk.models.version_list.VersionList
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query(self, *args, **kwargs)
Queries the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.metrics_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.metrics_client.html new file mode 100644 index 0000000..19f916f --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.metrics_client.html @@ -0,0 +1,129 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.metrics_client + + + + + +
 
ai_api_client_sdk.resource_clients.metrics_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/metrics_client.py
+

+

+ + + + + +
 
Modules
       
warnings
+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
MetricsClient +
+
+
+

+ + + + + + + +
 
class MetricsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   MetricsClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+MetricsClient is a class implemented for interacting with the metrics related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
MetricsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
delete(self, execution_id: str, resource_group: str = None) -> None
Deletes the metrics.

+:param execution_id: ID of the execution, of which the metrics should be deleted.
+:type execution_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+ +
query( + self, + filter: str = None, + execution_ids: List[str] = None, + select: List[str] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse
Queries the metrics.

+:param filter: Deprecated. Use parameter execution_ids instead. A filter expression that filters the metric
+    resources using execution IDs. User can only use in, eq operators in filter expression, defaults to None
+:type filter: str, optional
+:param execution_ids: IDs of the executions, of which the metrics should be retrieved, defaults to None
+:type execution_ids: List[str], optional
+:param select: Values of select can be metrics,tags,customInfo or any of the combinations of these or *. 
+    Can be used to select(project) only the resources specified
+:type select: List[str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.model_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.model_client.html new file mode 100644 index 0000000..dbec6b4 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.model_client.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.model_client + + + + + +
 
ai_api_client_sdk.resource_clients.model_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/model_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ModelClient +
+
+
+

+ + + + + + + +
 
class ModelClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ModelClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+ModelClient is a class implemented for interacting with model related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ModelClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
query(self, resource_group: str = None) -> ai_api_client_sdk.models.model_query_response.ModelQueryResponse
Queries the models.

+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.executable_query_response.ModelQueryResponse`
+ +
+Data and other attributes defined here:
+
DEFAULT_SCENARIO_ID = 'foundation-models'
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.resource_groups_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.resource_groups_client.html new file mode 100644 index 0000000..f56cdd2 --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.resource_groups_client.html @@ -0,0 +1,159 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.resource_groups_client + + + + + +
 
ai_api_client_sdk.resource_clients.resource_groups_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/resource_groups_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ResourceGroupsClient +
+
+
+

+ + + + + + + +
 
class ResourceGroupsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ResourceGroupsClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+ResourceGroupsClient is a class implemented for interacting with the resource groups endpoints of the server.
+It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ResourceGroupsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create( + self, + resource_group_id: str, + labels: List[ai_api_client_sdk.models.label.Label] = None +) -> ai_api_client_sdk.models.resource_group.ResourceGroup
Creates resource group for a given tenant.

+:param resource_group_id: the id of the resource group and the length must be between 3 and 10 characters.
+:type resource_group_id: str
+:param labels: key-value pairs of the labels that will be added to the resource group.
+:type labels: List[Label]
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup`
+ +
delete(self, resource_group_id: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the resource group.

+:param resource_group_id: the id of the resource group to be deleted
+:type resource_group_id: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, resource_group_id: str) -> ai_api_client_sdk.models.resource_group.ResourceGroup
Gets a resource group of a given tenant.

+:param resource_group_id: the id of the resource group to be retrieved
+:type resource_group_id: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the resource group from the server
+:rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup`
+ +
modify( + self, + resource_group_id: str, + labels: List[ai_api_client_sdk.models.label.Label] +) -> None
Modifies a resource group.

+:param resource_group_id: the id of the resource group
+:type resource_group_id: str
+:param labels: key-value pairs of the labels that will be added to the resource group.
+:type labels: List[Label]
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+ +
query(self, search: str = None, search_case_insensitive: bool = None) -> ai_api_client_sdk.models.resource_group_query_response.ResourceGroupQueryResponse
Get all resource groups.

+:param search: Generic search term to be looked for in various attributes of resource groups, defaults to None
+:type search: str, optional
+:param search_case_insensitive: Indicates whether the search should be case insensitive
+:type search_case_insensitive: bool, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of resource groups for a given tenant.
+:rtype: class:`ai_core_sdk.models.resource_group_query_response.ResourceGroupQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/base/docs/ai_api_client_sdk.resource_clients.scenario_client.html b/packages/base/docs/ai_api_client_sdk.resource_clients.scenario_client.html new file mode 100644 index 0000000..e33cb1a --- /dev/null +++ b/packages/base/docs/ai_api_client_sdk.resource_clients.scenario_client.html @@ -0,0 +1,150 @@ + + + + +Python: module ai_api_client_sdk.resource_clients.scenario_client + + + + + +
 
ai_api_client_sdk.resource_clients.scenario_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/base/ai_api_client_sdk/resource_clients/scenario_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ScenarioClient +
+
+
+

+ + + + + + + +
 
class ScenarioClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ScenarioClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+ScenarioClient is a class implemented for interacting with the scenario related endpoints of the server. It
+implements the base class :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ScenarioClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
get(self, scenario_id: str, resource_group: str = None) -> ai_api_client_sdk.models.scenario.Scenario
Retrieves the scenario from the server.

+:param scenario_id: ID of the scenario to be retrieved
+:type scenario_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved scenario
+:rtype: class:`ai_api_client_sdk.models.scenario.Scenario`
+ +
query(self, only_llm_scenarios: bool = False, resource_group: str = None) -> ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse
Queries the scenarios.

+:param only_llm_scenarios: indicates whether to query for LLM scenarios only, defaults to False
+:type only_llm_scenarios:  bool, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse`
+ +
query_llm_scenarios(self, resource_group: str = None) -> ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse
Queries for the LLM scenarios.

+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIRequestException` if an unexpected exception occurs while
+    trying to send a request to the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse`
+ +
query_versions( + self, + scenario_id: str, + label_selector: List[str] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.version_query_response.VersionQueryResponse
Queries the versions.

+:param scenario_id: ID of the scenario, the versions should belong to
+:type scenario_id: str
+:param label_selector: list of the label selector strings in the form of "key=value" or "key!=value", to filter
+    the scenarios with respect to their labels, defaults to None
+:type label_selector: List[str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.scenario_query_response.ScenarioQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/base/integration_tests/__init__.py b/packages/base/integration_tests/__init__.py new file mode 100644 index 0000000..af8010e --- /dev/null +++ b/packages/base/integration_tests/__init__.py @@ -0,0 +1,121 @@ +import os +import random +from time import sleep + +import requests + +from ai_api_client_sdk.helpers.authenticator import Authenticator + + +def get_random_string(l=4): + alphanumeric = 'abcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(random.choices(alphanumeric, k=l)) + + +def get_internalprod_cluster_info(): + auth_url = os.getenv('XSUAA_AUTH_URL') + client_id = os.getenv('XSUAA_CLIENT_ID') + client_secret = os.getenv('XSUAA_CLIENT_SECRET') + cluster_base_url = os.getenv('CLUSTER_BASE_URL') + base_url = f"{cluster_base_url}/v2/lm" + provisioning_base_url = f'{cluster_base_url}/v2/admin' + return base_url, auth_url, client_id, client_secret, provisioning_base_url + + +def get_x509_credentials(): + x509_cert_url = os.getenv(('XSUAA_X509_CERT_URL')) + x509_cert = os.getenv('XSUAA_X509_CERT') + x509_key = os.getenv('XSUAA_X509_KEY') + return x509_cert_url, x509_cert, x509_key + + +def get_s3_bucket_info(): + oss_key = os.getenv('OSS_KEY') + oss_bucket = os.getenv('OSS_BUCKET') + oss_endpoint = os.getenv('OSS_ENDPOINT') + oss_region = os.getenv('OSS_REGION') + oss_secret = os.getenv('OSS_SECRET') + return oss_bucket, oss_endpoint, oss_region, oss_key, oss_secret + + +def get_cluster_info(): + return get_internalprod_cluster_info() + + +def get_number_of_integration_tests(): + dir_path = os.path.dirname(__file__) + files = os.listdir(dir_path) + return len(list(filter(lambda x: x.startswith('test'), files))) + + +TENANT_ID = os.getenv('TEST_TENANT_ID') +RESOURCE_GROUP_ID = f'aicli{get_random_string()}' +BASE_URL, AUTH_URL, CLIENT_ID, CLIENT_SECRET, PROVISIONING_BASE_URL = get_cluster_info() +OSS_BUCKET, OSS_ENDPOINT, OSS_REGION, OSS_KEY, OSS_SECRET = get_s3_bucket_info() +X509_CERT_URL, X509_CERT_STR, X509_KEY_STR = get_x509_credentials() +dir_path = os.path.dirname(__file__) +X509_CERT_FILE_PATH = os.path.join(dir_path, 'x509_cert.pem') +X509_KEY_FILE_PATH = os.path.join(dir_path, 'x509_key.pem') + + +def get_token(): + return Authenticator(auth_url=AUTH_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET).get_token() + + +def write_x509_credentials_into_files(): + with open(X509_CERT_FILE_PATH, 'w') as f: + f.write(X509_CERT_STR) + with open(X509_KEY_FILE_PATH, 'w') as f: + f.write(X509_KEY_STR) + + +def remove_x509_credentials(): + os.remove(X509_CERT_FILE_PATH) + os.remove(X509_KEY_FILE_PATH) + + +def provision_resource_group(): + headers = {'Authorization': get_token()} + res = requests.post(url=f'{PROVISIONING_BASE_URL}/resourceGroups', json={"resourceGroupId": RESOURCE_GROUP_ID}, + headers=headers) + if res.status_code // 100 != 2: + raise Exception(f"Failed to create resource group {RESOURCE_GROUP_ID}: {res.status_code}, {res.text}") + + sleep(5) + for i in range(10): + res = requests.get(url=f'{PROVISIONING_BASE_URL}/resourceGroups/{RESOURCE_GROUP_ID}', headers=headers) + try: + if res.status_code == 200 and res.json()['status'] == 'PROVISIONED': + break + except Exception: + pass + sleep(0.5) + print(f"client sdk integration test resource group: {RESOURCE_GROUP_ID}") + headers['AI-Resource-Group'] = RESOURCE_GROUP_ID + res = requests.post(url=f'{PROVISIONING_BASE_URL}/objectStoreSecrets', + json={"name": "default", "type": 'S3', "bucket": OSS_BUCKET, "endpoint": OSS_ENDPOINT, + "pathPrefix": "", "region": OSS_REGION, + "data": {"AWS_ACCESS_KEY_ID": OSS_KEY, "AWS_SECRET_ACCESS_KEY": OSS_SECRET}}, + headers=headers) + if res.status_code // 100 != 2: + raise Exception( + f"Failed to create object store secret for {RESOURCE_GROUP_ID}: {res.status_code}, {res.text}") + + +def deprovision_resource_group(): + headers = {'Authorization': get_token()} + res = requests.delete(url=f'{PROVISIONING_BASE_URL}/resourceGroups/{RESOURCE_GROUP_ID}', headers=headers) + if res.status_code != 202: + raise Exception(f"Failed to remove resource group {RESOURCE_GROUP_ID}") + + +# This function will run before all tests in integration_tests module +def setUpModule(): + provision_resource_group() + write_x509_credentials_into_files() + + +# This function will run after all tests in integration_tests module +def tearDownModule(): + deprovision_resource_group() + remove_x509_credentials() diff --git a/packages/base/integration_tests/ai_api_v2_client_e2e_test_base.py b/packages/base/integration_tests/ai_api_v2_client_e2e_test_base.py new file mode 100644 index 0000000..016a663 --- /dev/null +++ b/packages/base/integration_tests/ai_api_v2_client_e2e_test_base.py @@ -0,0 +1,137 @@ +from datetime import timezone +from time import sleep +from typing import Any, Dict, List +from unittest import TestCase + +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.executable import Executable +from ai_api_client_sdk.models.input_artifact import InputArtifact +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.parameter import Parameter +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.resource_clients.base_client import BaseClient +from . import AUTH_URL, BASE_URL, CLIENT_ID, CLIENT_SECRET, RESOURCE_GROUP_ID + + +class AIAPIV2ClientE2ETestBase(TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # provision_resource_group() + cls.ai_api_v2_client = AIAPIV2Client(base_url=BASE_URL, auth_url=AUTH_URL, client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, resource_group=RESOURCE_GROUP_ID) + cls.test_scenario_id = '88888888-4444-4444-4444-cccccccccccc' + + @classmethod + def tearDownClass(cls) -> None: + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # deprovision_resource_group() + super().tearDownClass() + + def assert_object(self, d: Dict[str, Any], o: object): + for k in d.keys(): + self.assertEqual(d[k], getattr(o, k)) + + def assert_dicts_in_objects(self, dict_list: List[Dict[str, Any]], object_list: list, + key: str = 'id'): + dict_keys = [d[key] for d in dict_list] + object_list = list(filter(lambda o: getattr(o, key) in dict_keys, object_list)) + self.assertEqual(len(dict_list), len(object_list)) + dict_list = sorted(dict_list, key=lambda d: d[key]) + object_list = sorted(object_list, key=lambda o: getattr(o, key)) + for i in range(len(object_list)): + self.assert_object(dict_list[i], object_list[i]) + + def assert_datetime(self, response_obj_dt_field): + self.assertIsNotNone(response_obj_dt_field) + self.assertEqual(response_obj_dt_field.tzinfo, timezone.utc) + + def wait_until_enactment_has_status(self, resource_client: BaseClient, params: Dict[str, str], status: Status): + for _ in range(400): + enactment = resource_client.get(**params) + if enactment.status in [status, Status.DEAD]: + break + sleep(3) + print(enactment.status_details) + self.assertEqual(status, enactment.status) + return enactment + + def get_an_executable(self, deployable: bool = False, scenario_id: str = None) -> Executable: + if not scenario_id: + scenario_id = self.test_scenario_id + res = self.ai_api_v2_client.executable.query(scenario_id=scenario_id) + for e in res.resources: + if e.deployable == deployable: + return e + return None + + def create_artifact_dicts(self, n): + artifact_dicts = [] + for i in range(n): + artifact = self.create_artifact_dict(i) + artifact_dicts.append(artifact) + return artifact_dicts + + def create_artifact_dict(self, i=1, labels=None, kind: Artifact.Kind = Artifact.Kind.MODEL): + if not labels: + labels = [ + Label(**{ + "key": "ext.ai.sap.com/s4hana-version", + "value": "string" + }) + ] + return { + 'name': f'Test Artifact {i}', + 'kind': kind, + 'labels': labels, + 'url': 'gs://kfserving-examples/models/tensorflow/flowers', + 'scenario_id': self.test_scenario_id, + 'description': f'Test Artifact {i} description' + } + + def get_an_artifact(self) -> Artifact: + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id) + if res.count > 0: + return res.resources[0] + artifact_dict = self.create_artifact_dicts(1)[0] + res = self.ai_api_v2_client.artifact.create(**artifact_dict) + return self.ai_api_v2_client.artifact.get(artifact_id=res.id) + + def create_configuration_dicts(self, n: int, executable_id: str, parameters: List[Parameter] = None, + input_artifacts: List[InputArtifact] = None, scenario_id: str = None): + if scenario_id: + test_scenario_id = scenario_id + else: + test_scenario_id = self.test_scenario_id + + configuration_dicts = [] + for i in range(n): + configuration_dict = { + 'name': f'Test Configuration {i}', + 'scenario_id': test_scenario_id, + 'executable_id': executable_id + } + if parameters: + configuration_dict['parameter_bindings'] = [ + ParameterBinding(key=p.name, value=f'Test {p.name} value {i}') for p in parameters + ] + if input_artifacts: + artifact = self.get_an_artifact() + configuration_dict['input_artifact_bindings'] = [ + InputArtifactBinding(key=ia.name, artifact_id=artifact.id) for ia in input_artifacts + ] + configuration_dicts.append(configuration_dict) + return configuration_dicts + + def get_a_configuration(self, deployable: bool = False, scenario_id: str = None): + executable = self.get_an_executable(deployable=deployable, scenario_id=scenario_id) + cfg_dict = self.create_configuration_dicts(n=1, executable_id=executable.id, parameters=executable.parameters, + input_artifacts=executable.input_artifacts, scenario_id=scenario_id)[ + 0] + res = self.ai_api_v2_client.configuration.create(**cfg_dict) + return self.ai_api_v2_client.configuration.get(configuration_id=res.id) diff --git a/packages/base/integration_tests/test_e2e_artifacts.py b/packages/base/integration_tests/test_e2e_artifacts.py new file mode 100644 index 0000000..667ce4c --- /dev/null +++ b/packages/base/integration_tests/test_e2e_artifacts.py @@ -0,0 +1,125 @@ +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.label import Label +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EArtifacts(AIAPIV2ClientE2ETestBase): + + def test_artifacts(self): + n = 3 + artifact_dicts = self.create_artifact_dicts(n) + for ad in artifact_dicts: + res = self.ai_api_v2_client.artifact.create(**ad) + self.assertIsNotNone(res.id) + ad['id'] = res.id + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id) + self.assertTrue(res.count >= n) + self.assert_dicts_in_objects(artifact_dicts, res.resources) + self.assert_datetime(res.resources[0].created_at) + + search_string = 'Test Artifact' + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, search=search_string) + self.assertTrue(all(search_string in a.name or search_string in a.description for a in res.resources)) + + artifact_dict = artifact_dicts[n - 1] + artifact = self.ai_api_v2_client.artifact.get(artifact_id=artifact_dict['id']) + self.assert_object(artifact_dict, artifact) + self.assert_datetime(artifact.created_at) + self.assert_datetime(artifact.modified_at) + + res_count = self.ai_api_v2_client.artifact.count(scenario_id=self.test_scenario_id) + self.assertTrue(res_count >= n) + + def test_artifact_with_expand_scenario(self): + n = 3 + artifact_dicts = self.create_artifact_dicts(n) + for ad in artifact_dicts: + res = self.ai_api_v2_client.artifact.create(**ad) + self.assertIsNotNone(res.id) + ad['id'] = res.id + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, expand='scenario') + self.assertTrue(res.count >= n) + self.assert_dicts_in_objects(artifact_dicts, res.resources) + self.assertTrue('scenario' in a for a in res.resources) + + artifact_dict = artifact_dicts[n - 1] + artifact = self.ai_api_v2_client.artifact.get(artifact_id=artifact_dict['id'], expand='scenario') + self.assert_object(artifact_dict, artifact) + self.assertIsNotNone(artifact.scenario) + self.assertIsNotNone(artifact.scenario.id) + self.assertIsNotNone(artifact.scenario.name) + self.assert_datetime(artifact.scenario.created_at) + self.assert_datetime(artifact.scenario.modified_at) + + def test_artifact_kind_other(self): + ad = self.create_artifact_dict(1, kind=Artifact.Kind.OTHER) + res = self.ai_api_v2_client.artifact.create(**ad) + self.assertIsNotNone(res.id) + + ad['id'] = res.id + + artifact = self.ai_api_v2_client.artifact.get(artifact_id=ad['id']) + self.assert_object(ad, artifact) + + def test_filter_artifacts_by_labels(self): + artifact_dicts = [ + self.create_artifact_dict(labels=[Label(**{'key': 'ext.ai.sap.com/s4hana-version', 'value': 'string'})]), + self.create_artifact_dict(labels=[Label(**{'key': 'ext.ai.sap.com/s4hana-version', 'value': 'string2'}), + Label(**{'key': 'ext.ai.sap.com/dummy', 'value': 'dummy'})]), + self.create_artifact_dict(labels=[Label(**{'key': 'ext.ai.sap.com/s4hana-version', 'value': 'string2'})]) + ] + for artifact_dict in artifact_dicts: + res = self.ai_api_v2_client.artifact.create(**artifact_dict) + self.assertIsNotNone(res.id) + artifact_dict['id'] = res.id + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string') + self.assertTrue(res.count >= 1) + self.assertEqual(res.count, len(res.resources)) + res_ids = [artifact.id for artifact in res.resources] + self.assertIn(artifact_dicts[0]['id'], res_ids) + self.assertNotIn(artifact_dicts[1]['id'], res_ids) + self.assertNotIn(artifact_dicts[2]['id'], res_ids) + + res_count = self.ai_api_v2_client.artifact.count(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string2') + self.assertTrue(res_count >= 1) + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string2') + self.assertTrue(res.count >= 1) + self.assertEqual(res.count, len(res.resources)) + res_ids = [artifact.id for artifact in res.resources] + self.assertIn(artifact_dicts[1]['id'], res_ids) + self.assertIn(artifact_dicts[2]['id'], res_ids) + self.assertNotIn(artifact_dicts[0]['id'], res_ids) + + res_count = self.ai_api_v2_client.artifact.count(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string2') + self.assertTrue(res_count >= 1) + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string2,ext.ai.sap.com/dummy=dummy') + self.assertTrue(res.count >= 1) + res_ids = [artifact.id for artifact in res.resources] + self.assertIn(artifact_dicts[1]['id'], res_ids) + self.assertNotIn(artifact_dicts[0]['id'], res_ids) + self.assertNotIn(artifact_dicts[2]['id'], res_ids) + + res_count = self.ai_api_v2_client.artifact.count(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/s4hana-version=string2,ext.ai.sap.com/dummy=dummy') + self.assertTrue(res_count >= 1) + + res = self.ai_api_v2_client.artifact.query(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/dummy!=dummy') + self.assertEqual(res.count, len(res.resources)) + res_ids = [artifact.id for artifact in res.resources] + for artifact in artifact_dicts: + self.assertNotIn(artifact['id'], res_ids) + + res_count = self.ai_api_v2_client.artifact.count(scenario_id=self.test_scenario_id, + artifact_label_selector='ext.ai.sap.com/dummy!=dummy') + self.assertEqual(res_count, len(res.resources)) diff --git a/packages/base/integration_tests/test_e2e_client_type.py b/packages/base/integration_tests/test_e2e_client_type.py new file mode 100644 index 0000000..b105dc9 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_client_type.py @@ -0,0 +1,50 @@ +import os +from unittest.mock import patch + +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client +from integration_tests.ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EClientType(AIAPIV2ClientE2ETestBase): + + auth_url = os.getenv('XSUAA_AUTH_URL') + client_id = os.getenv('XSUAA_CLIENT_ID') + client_secret = os.getenv('XSUAA_CLIENT_SECRET') + cluster_base_url = os.getenv('CLUSTER_BASE_URL') + base_url = f"{cluster_base_url}/v2/lm" + resource_group = "resource_group_for_client_type_test" + + def test_client_type_from_parameter(self): + client_type = 'test_client_type' + ai_api_v2_client = AIAPIV2Client( + base_url=self.base_url, + auth_url=self.auth_url, + client_id=self.client_id, + client_secret=self.client_secret, + resource_group=self.resource_group, + client_type=client_type + ) + rest_client = ai_api_v2_client.rest_client + headers = rest_client.headers + self.assertEqual(client_type, headers['AI-Client-Type']) + + def test_client_type_from_env_var(self): + backup = os.environ.get('AI_CLIENT_TYPE', None) + try: + env_client_type = 'env_client_type' + os.environ['AI_CLIENT_TYPE'] = env_client_type + ai_api_v2_client = AIAPIV2Client( + base_url=self.base_url, + auth_url=self.auth_url, + client_id=self.client_id, + client_secret=self.client_secret, + resource_group=self.resource_group + ) + rest_client = ai_api_v2_client.rest_client + headers = rest_client.headers + self.assertEqual(env_client_type, headers['AI-Client-Type']) + finally: + if backup is not None: + os.environ['AI_CLIENT_TYPE'] = backup + else: + del os.environ['AI_CLIENT_TYPE'] diff --git a/packages/base/integration_tests/test_e2e_configurations.py b/packages/base/integration_tests/test_e2e_configurations.py new file mode 100644 index 0000000..6170b0f --- /dev/null +++ b/packages/base/integration_tests/test_e2e_configurations.py @@ -0,0 +1,82 @@ +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EConfigurations(AIAPIV2ClientE2ETestBase): + + def test_configurations(self): + n = 3 + configuration_dicts = [] + executable_d = self.get_an_executable(deployable=True) + configuration_dicts.extend(self.create_configuration_dicts(n=n, executable_id=executable_d.id, + parameters=executable_d.parameters, + input_artifacts=executable_d.input_artifacts)) + executable_e = self.get_an_executable(deployable=False) + configuration_dicts.extend(self.create_configuration_dicts(n=n, executable_id=executable_e.id, + parameters=executable_e.parameters)) + for configuration_dict in configuration_dicts: + res = self.ai_api_v2_client.configuration.create(**configuration_dict) + self.assertIsNotNone(res.id) + configuration_dict['id'] = res.id + + res = self.ai_api_v2_client.configuration.query(scenario_id=self.test_scenario_id) + self.assertTrue(res.count >= 2 * n) + self.assert_dicts_in_objects(configuration_dicts, res.resources) + self.assert_datetime(res.resources[0].created_at) + + res_count = self.ai_api_v2_client.configuration.count(scenario_id=self.test_scenario_id) + self.assertTrue(res_count >= 2 * n) + + res = self.ai_api_v2_client.configuration.query(executable_ids=[executable_e.id, executable_d.id]) + self.assertTrue(res.count >= 2 * n) + self.assert_dicts_in_objects(configuration_dicts, res.resources) + + res_count = self.ai_api_v2_client.configuration.count(executable_ids=[executable_e.id, executable_d.id]) + self.assertTrue(res_count >= 2 * n) + + res = self.ai_api_v2_client.configuration.query(executable_ids=[executable_e.id]) + self.assertTrue(res.count >= n) + self.assert_dicts_in_objects(configuration_dicts[n:], res.resources) + + res_count = self.ai_api_v2_client.configuration.count(executable_ids=[executable_e.id]) + self.assertTrue(res_count >= n) + + search_string = 'Test Configuration' + res = self.ai_api_v2_client.configuration.query(scenario_id=self.test_scenario_id, search=search_string) + self.assertTrue(res.count >= 2 * n) + self.assertTrue(all(search_string in c.name for c in res.resources)) + + res_count = self.ai_api_v2_client.configuration.count(scenario_id=self.test_scenario_id, search=search_string) + self.assertTrue(res_count >= 2 * n) + + configuration_dict = configuration_dicts[n - 1] + configuration = self.ai_api_v2_client.configuration.get(configuration_id=configuration_dict['id']) + self.assert_object(configuration_dict, configuration) + self.assert_datetime(configuration.created_at) + + def test_configuration_with_expand_scenario(self): + n = 3 + configuration_dicts = [] + executable_d = self.get_an_executable() + configuration_dicts.extend(self.create_configuration_dicts(n=n, executable_id=executable_d.id, + parameters=executable_d.parameters, + input_artifacts=executable_d.input_artifacts)) + + for configuration_dict in configuration_dicts: + res = self.ai_api_v2_client.configuration.create(**configuration_dict) + self.assertIsNotNone(res.id) + configuration_dict['id'] = res.id + + res = self.ai_api_v2_client.configuration.query(scenario_id=self.test_scenario_id, expand='scenario') + self.assertTrue(res.count >= n) + self.assert_dicts_in_objects(configuration_dicts, res.resources) + self.assertTrue('scenario' in a for a in res.resources) + + configuration_dict = configuration_dicts[n - 1] + configuration = self.ai_api_v2_client.configuration.get(configuration_id=configuration_dict['id'], + expand='scenario') + self.assert_object(configuration_dict, configuration) + self.assertIsNotNone(configuration.scenario) + self.assertIsNotNone(configuration.scenario.id) + self.assertIsNotNone(configuration.scenario.name) + self.assertIsNotNone(configuration.scenario.created_at) + self.assertIsNotNone(configuration.scenario.modified_at) diff --git a/packages/base/integration_tests/test_e2e_deployments.py b/packages/base/integration_tests/test_e2e_deployments.py new file mode 100644 index 0000000..496649a --- /dev/null +++ b/packages/base/integration_tests/test_e2e_deployments.py @@ -0,0 +1,94 @@ +import uuid + +from ai_api_client_sdk.models.base_models import BasicModifyRequest +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EDeployments(AIAPIV2ClientE2ETestBase): + def test_deployments(self): + configuration = self.get_a_configuration(deployable=True) + n = 2 + deployment_dicts = [] + for _ in range(n): + res = self.ai_api_v2_client.deployment.create(configuration_id=configuration.id) + deployment_dicts.append({ + 'id': res.id, + 'configuration_id': configuration.id, + 'configuration_name': configuration.name, + 'scenario_id': self.test_scenario_id + }) + for dep_dict in deployment_dicts: + dep = self.ai_api_v2_client.deployment.get(deployment_id=dep_dict['id']) + self.assert_object(dep_dict, dep) + self.assert_datetime(dep.created_at) + dep_dict['created_at'] = dep.created_at + self.assert_datetime(dep.modified_at) + self.assertEqual(TargetStatus.RUNNING, dep.target_status) + dep_dict['target_status'] = dep.target_status + dep = self.wait_until_enactment_has_status(resource_client=self.ai_api_v2_client.deployment, + params={'deployment_id': dep_dict['id']}, status=Status.RUNNING) + dep_dict['status'] = dep.status + self.assertIsNotNone(dep.deployment_url) + self.assertNotEqual('', dep.deployment_url) + dep_dict['deployment_url'] = dep.deployment_url + + dep = self.ai_api_v2_client.deployment.get(deployment_id=dep_dict['id'], select='status') + self.assertEqual(dep_dict['status'], dep.status) + self.assertFalse(hasattr(dep, dep_dict['deployment_url'])) + self.assertFalse(hasattr(dep, dep_dict['configuration_name'])) + + logs = self.ai_api_v2_client.deployment.query_logs(deployment_id=dep_dict['id']) + self.assertIsNotNone(logs.data.result) + + res = self.ai_api_v2_client.deployment.query(scenario_id=self.test_scenario_id, + configuration_id=configuration.id, + executable_ids=[configuration.executable_id], + status=Status.RUNNING) + self.assertTrue(res.count >= n) + self.assert_datetime(res.resources[0].created_at) + self.assert_dicts_in_objects(deployment_dicts, res.resources) + + res_count = self.ai_api_v2_client.deployment.count(scenario_id=self.test_scenario_id, + configuration_id=configuration.id, + executable_ids=[configuration.executable_id], + status=Status.RUNNING) + self.assertTrue(res_count >= n) + + # modify deployment with new configuration_id + dep_dict = deployment_dicts[0] + new_conf = self.get_a_configuration(deployable=True) + self.ai_api_v2_client.deployment.modify(deployment_id=dep_dict['id'], configuration_id=new_conf.id) + dep = self.ai_api_v2_client.deployment.get(deployment_id=dep_dict['id']) + self.assertEqual(new_conf.id, dep.configuration_id) + self.assertEqual(configuration.id, dep.latest_running_configuration_id) + dep = self.wait_until_enactment_has_status(resource_client=self.ai_api_v2_client.deployment, + params={'deployment_id': dep_dict['id']}, status=Status.RUNNING) + self.assertEqual(Status.RUNNING, dep.status) + self.assertIsNotNone(dep.deployment_url) + self.assertNotEqual('', dep.deployment_url) + + # bulk modify deployments + deployments = [ + BasicModifyRequest(deployment_dicts[0]['id'], TargetStatus.STOPPED), + BasicModifyRequest(str(uuid.uuid4()), TargetStatus.STOPPED) + ] + dep_bulk_modify_response = self.ai_api_v2_client.deployment.bulk_modify(deployments=deployments) + self.assertEqual("Deployment modification scheduled", dep_bulk_modify_response.deployments[0].message) + self.assertEqual("01010016", dep_bulk_modify_response.deployments[1].error.code) + + def test_deployments_with_ttl(self): + configuration = self.get_a_configuration(deployable=True) + ttl = "10h" + res = self.ai_api_v2_client.deployment.create(configuration_id=configuration.id, ttl=ttl) + dep_dict = { + 'id': res.id, + 'configuration_id': configuration.id, + 'configuration_name': configuration.name, + 'scenario_id': self.test_scenario_id + } + dep = self.ai_api_v2_client.deployment.get(deployment_id=res.id) + self.assert_object(dep_dict, dep) + self.assertIsNotNone(dep.ttl) + self.assertEqual(dep.ttl, ttl) diff --git a/packages/base/integration_tests/test_e2e_executables.py b/packages/base/integration_tests/test_e2e_executables.py new file mode 100644 index 0000000..b4c0751 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_executables.py @@ -0,0 +1,20 @@ +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EExecutables(AIAPIV2ClientE2ETestBase): + def test_query_and_get_executables(self): + res = self.ai_api_v2_client.executable.query(scenario_id=self.test_scenario_id) + self.assertEqual(res.count, len(res.resources)) + self.assertTrue(res.count > 0) + executables = res.resources + self.assert_datetime(executables[0].created_at) + executable = self.ai_api_v2_client.executable.get(scenario_id=executables[0].scenario_id, + executable_id=executables[0].id) + self.assertEqual(executables[0], executable) + self.assertIsNotNone(executable.id) + self.assertIsNotNone(executable.scenario_id) + self.assertIsNotNone(executable.version_id) + self.assertIsNotNone(executable.name) + self.assertIsNotNone(executable.deployable) + self.assert_datetime(executable.created_at) + self.assert_datetime(executable.modified_at) diff --git a/packages/base/integration_tests/test_e2e_executions.py b/packages/base/integration_tests/test_e2e_executions.py new file mode 100644 index 0000000..3195809 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_executions.py @@ -0,0 +1,61 @@ +from ai_api_client_sdk.models.base_models import BasicModifyRequest +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EExecutions(AIAPIV2ClientE2ETestBase): + def test_executions(self): + configuration = self.get_a_configuration(deployable=False) + n = 3 + execution_dicts = [] + for _ in range(n): + res = self.ai_api_v2_client.execution.create(configuration_id=configuration.id) + execution_dicts.append({ + 'id': res.id, + 'configuration_id': configuration.id, + 'configuration_name': configuration.name, + 'scenario_id': self.test_scenario_id + }) + for execution_dict in execution_dicts: + execution = self.ai_api_v2_client.execution.get(execution_id=execution_dict['id']) + self.assert_object(execution_dict, execution) + self.assert_datetime(execution.created_at) + execution_dict['created_at'] = execution.created_at + self.assert_datetime(execution.modified_at) + self.assertEqual(TargetStatus.COMPLETED, execution.target_status) + execution_dict['target_status'] = execution.target_status + execution = self.wait_until_enactment_has_status(resource_client=self.ai_api_v2_client.execution, + params={'execution_id': execution_dict['id']}, + status=Status.COMPLETED) + execution_dict['status'] = execution.status + + exc = self.ai_api_v2_client.execution.get(execution_id=execution_dict['id'], select='status') + self.assertEqual(execution_dict['status'], exc.status) + self.assertFalse(hasattr(exc, execution_dict['configuration_name'])) + self.assertFalse(hasattr(exc, execution_dict['scenario_id'])) + + logs = self.ai_api_v2_client.execution.query_logs(execution_id=execution_dict['id']) + self.assertIsNotNone(logs.data.result) + + res = self.ai_api_v2_client.execution.query(scenario_id=self.test_scenario_id, + configuration_id=configuration.id, + executable_ids=[configuration.executable_id], + status=Status.COMPLETED) + self.assertTrue(res.count >= n) + self.assert_dicts_in_objects(execution_dicts, res.resources) + + res_count = self.ai_api_v2_client.execution.count(scenario_id=self.test_scenario_id, + configuration_id=configuration.id, + executable_ids=[configuration.executable_id], + status=Status.COMPLETED) + self.assertTrue(res_count >= n) + + # bulk modify executions + executions = [ + BasicModifyRequest(execution_dicts[0]['id'], TargetStatus.DELETED), + BasicModifyRequest("exec_a_not_exist", TargetStatus.DELETED) + ] + response = self.ai_api_v2_client.execution.bulk_modify(executions=executions) + self.assertEqual("Execution modification scheduled", response.executions[0].message) + self.assertEqual("01010020", response.executions[1].error.code) diff --git a/packages/base/integration_tests/test_e2e_executions_schedules.py b/packages/base/integration_tests/test_e2e_executions_schedules.py new file mode 100644 index 0000000..edbe6ac --- /dev/null +++ b/packages/base/integration_tests/test_e2e_executions_schedules.py @@ -0,0 +1,67 @@ +from ai_api_client_sdk.models.status import ScheduleStatus +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EExecutionSchedules(AIAPIV2ClientE2ETestBase): + def test_execution_schedules(self): + es_name = 'test execution schedule' + es_cron = '1 1 1 1 1' + configuration = self.get_a_configuration(deployable=False) + + # *** create *** + res = self.ai_api_v2_client.execution_schedule.create(name=es_name, cron=es_cron, + configuration_id=configuration.id) + es_dict = { + 'id': res.id, + 'configuration_id': configuration.id, + 'cron': es_cron, + 'name': es_name + } + execution_schedule = self.ai_api_v2_client.execution_schedule.get(execution_schedule_id=res.id) + self.assert_datetime(execution_schedule.created_at) + self.assert_datetime(execution_schedule.modified_at) + self.assert_object(es_dict, execution_schedule) + + # *** modify *** + es_dict['cron'] = '0 0 * * *' + es_dict['status'] = ScheduleStatus.INACTIVE + res_mod = self.ai_api_v2_client.execution_schedule.modify(execution_schedule_id=res.id, + cron=es_dict['cron'], + status=es_dict['status'] + ) + self.assertEqual(res_mod.id, res.id) + self.assertIn("modified", res_mod.message) + execution_schedule = self.ai_api_v2_client.execution_schedule.get(execution_schedule_id=res.id) + self.assert_datetime(execution_schedule.modified_at) + es_dict['modified_at'] = execution_schedule.modified_at + self.assert_object(es_dict, execution_schedule) + + # *** query *** + es_name2 = es_name + '2' + configuration2 = self.get_a_configuration(deployable=False) + res2 = self.ai_api_v2_client.execution_schedule.create(name=es_name2, cron=es_cron, + configuration_id=configuration2.id) + res_qry = self.ai_api_v2_client.execution_schedule.query(top=10) + self.assertEqual(res_qry.count, 2) + self.assert_datetime(res_qry.resources[0].created_at) + + execution_schedule2 = self.ai_api_v2_client.execution_schedule.get(execution_schedule_id=res2.id) + self.assert_datetime(execution_schedule2.created_at) + es_dict2 = { + 'id': res2.id, + 'configuration_id': configuration2.id, + 'cron': es_cron, + 'name': es_name2, + 'status': ScheduleStatus.ACTIVE, + 'created_at': execution_schedule2.created_at + } + self.assert_dicts_in_objects([es_dict, es_dict2], res_qry.resources) + + # *** count *** + res_count = self.ai_api_v2_client.execution_schedule.count(status=ScheduleStatus.INACTIVE) + self.assertEqual(res_count, 1) + + # *** delete *** + res_del = self.ai_api_v2_client.execution_schedule.delete(execution_schedule_id=res2.id) + self.assertEqual(res_del.id, res2.id) + self.assertIn("deleted", res_del.message) diff --git a/packages/base/integration_tests/test_e2e_healthz.py b/packages/base/integration_tests/test_e2e_healthz.py new file mode 100644 index 0000000..e9889c2 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_healthz.py @@ -0,0 +1,9 @@ +from ai_api_client_sdk.models.healthz_status import HealthStatus +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EHealthz(AIAPIV2ClientE2ETestBase): + def test_healthz(self): + healthz_status = self.ai_api_v2_client.healthz.get() + self.assertEqual(HealthStatus.READY, healthz_status.status) + self.assertIsNotNone(healthz_status.message) diff --git a/packages/base/integration_tests/test_e2e_meta.py b/packages/base/integration_tests/test_e2e_meta.py new file mode 100644 index 0000000..5668f3a --- /dev/null +++ b/packages/base/integration_tests/test_e2e_meta.py @@ -0,0 +1,33 @@ +from typing import List + +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EMeta(AIAPIV2ClientE2ETestBase): + + def assert_attributes_not_none(self, obj: object, attributes: List[str]): + for a in attributes: + self.assertIsNotNone(getattr(obj, a)) + + def assert_attributes_true(self, obj: object, attributes: List[str]): + for a in attributes: + self.assertTrue(getattr(obj, a)) + + def test_meta(self): + capabilities = self.ai_api_v2_client.meta.get() + self.assert_attributes_not_none(capabilities, ['ai_api', 'runtime_identifier', 'runtime_api_version']) + self.assert_attributes_not_none(capabilities.ai_api, ['capabilities', 'limits', 'version']) + self.assert_attributes_not_none(capabilities.ai_api.capabilities, ['logs', 'multitenant','bulk_updates']) + self.assert_attributes_true(capabilities.ai_api.capabilities, + ['user_executions', 'shareable', 'static_deployments', 'user_deployments', + 'time_to_live_deployments', 'execution_schedules']) + self.assert_attributes_true(capabilities.ai_api.capabilities.bulk_updates, ['executions', 'deployments']) + self.assert_attributes_true(capabilities.ai_api.capabilities.logs, ['deployments', 'executions']) + self.assert_attributes_not_none(capabilities.ai_api.limits, ['deployments', 'executions']) + self.assertEqual(capabilities.ai_api.limits.executions.max_running_count, -1) + self.assertEqual(capabilities.ai_api.limits.deployments.max_running_count, -1) + + def test_versions(self): + version_list = self.ai_api_v2_client.meta.get_versions() + self.assertIsNotNone(version_list.versions) + self.assert_attributes_not_none(version_list.versions[0], ['version_id', 'url', 'description']) diff --git a/packages/base/integration_tests/test_e2e_metrics.py b/packages/base/integration_tests/test_e2e_metrics.py new file mode 100644 index 0000000..3075dc1 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_metrics.py @@ -0,0 +1,124 @@ +import requests + +from ai_api_client_sdk.exception import AIAPINotFoundException, AIAPIInvalidRequestException +from . import get_token, RESOURCE_GROUP_ID, BASE_URL +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EMetrics(AIAPIV2ClientE2ETestBase): + def setUp(self) -> None: + super().setUp() + configuration = self.get_a_configuration(deployable=False) + res = self.ai_api_v2_client.execution.create(configuration_id=configuration.id) + self.execution_id = res.id + self.__post_metrics_for_execution(self.execution_id) + + def test_query_metrics(self): + with self.assertRaises(AIAPINotFoundException): + execution_id = "not-exist" + self.ai_api_v2_client.metrics.query(execution_ids=[execution_id]) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id]) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id) + response = self.ai_api_v2_client.metrics.query(filter=f"executionId eq '{self.execution_id}'") + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id) + with self.assertRaises(AIAPIInvalidRequestException): + self.ai_api_v2_client.metrics.query(filter=f"executionId eq '{self.execution_id}'", + execution_ids=[self.execution_id]) + + def test_query_metrics_with_select(self): + select = ['metrics', 'tags'] + with self.assertRaises(AIAPINotFoundException): + execution_id = "not-exist" + self.ai_api_v2_client.metrics.query(execution_ids=[execution_id], select=select) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id], select=select) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id, select) + select = ['metrics', 'customInfo'] + with self.assertRaises(AIAPINotFoundException): + execution_id = "not-exist" + self.ai_api_v2_client.metrics.query(execution_ids=[execution_id], select=select) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id], select=select) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id, select) + select = ['metrics'] + with self.assertRaises(AIAPINotFoundException): + execution_id = "not-exist" + self.ai_api_v2_client.metrics.query(execution_ids=[execution_id], select=select) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id], select=select) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id, select) + select = ['*'] + with self.assertRaises(AIAPINotFoundException): + execution_id = "not-exist" + self.ai_api_v2_client.metrics.query(execution_ids=[execution_id], select=select) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id], select=select) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id, select) + + def test_delete_metrics(self): + with self.assertRaises(AIAPINotFoundException): + execution_id = "aa97b177-9383-4934-8543-0f91b7a0283a" + self.ai_api_v2_client.metrics.delete(execution_id=execution_id) + response = self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id]) + metric = response.resources[0] + self.__assert_metric_returned(metric, self.execution_id) + self.ai_api_v2_client.metrics.delete(execution_id=self.execution_id) + with self.assertRaises(AIAPINotFoundException): + self.ai_api_v2_client.metrics.query(execution_ids=[self.execution_id]) + + def __assert_metric_returned(self, metric, execution_id, select: str = None): + self.assertEqual(metric.execution_id, execution_id) + probable_element_list = ['metrics', 'tags', 'customInfo'] + if not select or '*' in select: + select = 'metrics,tags,customInfo' + for select_value in select: + if select_value == 'metrics': + self.assertIsNotNone(metric.metrics) + if select_value == 'tags': + self.assertIsNotNone(metric.tags) + if select_value == 'customInfo': + self.assertIsNotNone(metric.custom_info) + for probable_element in probable_element_list: + if probable_element not in select: + if probable_element == 'customInfo': + probable_element = 'custom_info' + self.assertIsNone(getattr(metric, probable_element, None)) + + @staticmethod + def __post_metrics_for_execution(execution_id): + headers = {'Authorization': get_token(), 'AI-Resource-Group': RESOURCE_GROUP_ID} + metrics = { + "executionId": execution_id, + "metrics": [ + { + "name": "Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19.412Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Artifact Group", + "value": "RFC-1" + } + ], + "customInfo": [ + { + "name": "Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + response = requests.patch(url=f'{BASE_URL}/metrics', json=metrics, headers=headers) + if response.status_code != 204: + raise Exception(f"Failed to post metrics for execution {execution_id}") + print(f'Successfully add metrics for execution {execution_id} in testing') diff --git a/packages/base/integration_tests/test_e2e_model.py b/packages/base/integration_tests/test_e2e_model.py new file mode 100644 index 0000000..43c96b2 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_model.py @@ -0,0 +1,14 @@ +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EModel(AIAPIV2ClientE2ETestBase): + def test_query_models(self): + res = self.ai_api_v2_client.model.query() + self.assertEqual(res.count, len(res.resources)) + self.assertGreater(res.count, 0) + model = res.resources[0] + + self.assertIsNotNone(model.executable_id) + self.assertIsNotNone(model.model) + self.assertIsNotNone(model.description) + self.assertIsNotNone(model.versions) diff --git a/packages/base/integration_tests/test_e2e_resource_group.py b/packages/base/integration_tests/test_e2e_resource_group.py new file mode 100644 index 0000000..6f74a69 --- /dev/null +++ b/packages/base/integration_tests/test_e2e_resource_group.py @@ -0,0 +1,95 @@ +import time + +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.resource_group import ResourceGroup +from . import get_random_string +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EResourceGroup(AIAPIV2ClientE2ETestBase): + + @staticmethod + def _get_resource_group(resource_group_id: str): + return { + 'resource_group_id': resource_group_id, + 'labels': [ + Label(key="ext.ai.sap.com/label1", value="value1"), + Label(key="ext.ai.sap.com/label2", value="value2"), + ], + } + + def create_resource_group(self, resource_group_id: str): + rg_dict = self._get_resource_group(resource_group_id) + response = self.ai_api_v2_client.resource_groups.create( + resource_group_id=rg_dict['resource_group_id'], + labels=rg_dict['labels']) + self.assertEqual(rg_dict['resource_group_id'], response.resource_group_id) + # During create, the resource group labels are not returned, so we can not test them here. + + def delete_resource_group(self, resource_group_id: str): + response = self.ai_api_v2_client.resource_groups.delete( + resource_group_id=resource_group_id) + self.assertEqual(resource_group_id, response.id) + self.assertIsNotNone(response.message) + + def get_resource_group(self, resource_group_id: str): + # Poll until the resource group's status becomes PROVISIONED or until timeout. + timeout_seconds = 60 + poll_interval = 2 + deadline = time.time() + timeout_seconds + response = None + while time.time() <= deadline: + response = self.ai_api_v2_client.resource_groups.get( + resource_group_id=resource_group_id) + if response.status == 'PROVISIONED': + break + time.sleep(poll_interval) + # The resource group should reach PROVISIONED status within the timeout. + self.assertEqual('PROVISIONED', response.status) + rg_dict = self._get_resource_group(resource_group_id) + # We can only be sure about these two fields. + self.assertEqual(rg_dict['resource_group_id'], response.resource_group_id) + self.assertEqual(rg_dict['labels'], response.labels) + # The next fields have values that are not influenced by us. + # However, these values must exist. + self.assertIsNotNone(response.status) + self.assert_datetime(response.created_at) + + def modify_resource_group(self, resource_group_id: str): + labels = [ + Label(key="ext.ai.sap.com/label10", value="value10"), + ] + + self.ai_api_v2_client.resource_groups.modify( + resource_group_id=resource_group_id, + labels=labels) + + # Poll until the resource groups' lables changed or until timeout. + timeout_seconds = 60 + poll_interval = 2 + deadline = time.time() + timeout_seconds + response = None + while time.time() <= deadline: + response = self.ai_api_v2_client.resource_groups.get( + resource_group_id=resource_group_id) + if {l.key: l.value for l in labels} == {l.key: l.value for l in response.labels}: + break + time.sleep(poll_interval) + # Check that the modification was sucessfull. + self.assertEqual({l.key: l.value for l in labels}, {l.key: l.value for l in response.labels}) + + def query_resource_group(self): + response = self.ai_api_v2_client.resource_groups.query() + self.assertGreaterEqual(response.count, 1) + self.assertGreaterEqual(len(response.resources), 1) + self.assertIsInstance(response.resources[0], ResourceGroup) + self.assert_datetime(response.resources[0].created_at) + + def test_resource_groups(self): + resource_group_id = f'trg{get_random_string()[:5]}' + self.create_resource_group(resource_group_id) + self.get_resource_group(resource_group_id) + self.query_resource_group() + self.modify_resource_group(resource_group_id) + time.sleep(3) + self.delete_resource_group(resource_group_id) diff --git a/packages/base/integration_tests/test_e2e_scenarios.py b/packages/base/integration_tests/test_e2e_scenarios.py new file mode 100644 index 0000000..9b718dc --- /dev/null +++ b/packages/base/integration_tests/test_e2e_scenarios.py @@ -0,0 +1,68 @@ +from typing import List + +from ai_api_client_sdk.models.scenario import Scenario +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EScenarios(AIAPIV2ClientE2ETestBase): + + @staticmethod + def _get_scenario_from_scenarios(scenarios: List[Scenario], scenario_id: str): + for s in scenarios: + if s.id == scenario_id: + return s + return None + + def test_query_and_get_scenarios(self): + response = self.ai_api_v2_client.scenario.query() + scenarios = response.resources + self.assertEqual(response.count, len(scenarios)) + queried_scenario = self._get_scenario_from_scenarios(scenarios, self.test_scenario_id) + scenario = self.ai_api_v2_client.scenario.get(scenario_id=self.test_scenario_id) + self.assertEqual(queried_scenario, scenario) + self.assertIsNotNone(scenario.id) + self.assertIsNotNone(scenario.name) + self.assert_datetime(scenario.created_at) + self.assert_datetime(scenario.modified_at) + + def test_query_and_get_scenarios_for_llm(self): + response = self.ai_api_v2_client.scenario.query(only_llm_scenarios=True) + scenarios = response.resources + self.assertEqual(response.count, len(scenarios)) + llm_scenario_id = 'foundation-models' + queried_scenario = self._get_scenario_from_scenarios(scenarios, llm_scenario_id) + scenario = self.ai_api_v2_client.scenario.get(scenario_id=llm_scenario_id) + self.assertEqual(queried_scenario, scenario) + self.assertIsNotNone(scenario.id) + self.assertIsNotNone(scenario.name) + self.assert_datetime(scenario.created_at) + self.assert_datetime(scenario.modified_at) + self.assertIsNotNone(scenario.labels) + + def test_query_llm_scenarios(self): + response = self.ai_api_v2_client.scenario.query_llm_scenarios() + scenarios = response.resources + self.assertEqual(response.count, len(scenarios)) + scenario = scenarios[0] + self.assertIsNotNone(scenario.id) + self.assertIsNotNone(scenario.name) + self.assert_datetime(scenario.created_at) + self.assert_datetime(scenario.modified_at) + self.assertIsNotNone(scenario.labels) + + def test_query_versions(self): + version_response = self.ai_api_v2_client.scenario.query_versions(scenario_id=self.test_scenario_id) + self.assertTrue(version_response.count > 0) + self.assertEqual(version_response.count, len(version_response.resources)) + version = version_response.resources[0] + self.assertIsNotNone(version.id) + self.assertIsNotNone(version.scenario_id) + self.assert_datetime(version.created_at) + self.assert_datetime(version.modified_at) + + def test_is_llm_scenario(self): + llm_scenario_id = 'foundation-models' + scenario = self.ai_api_v2_client.scenario.get(scenario_id=llm_scenario_id) + self.assertTrue(scenario.is_llm_scenario()) + scenario = self.ai_api_v2_client.scenario.get(scenario_id=self.test_scenario_id) + self.assertFalse(scenario.is_llm_scenario()) diff --git a/packages/base/integration_tests/test_e2e_x509_credentials.py b/packages/base/integration_tests/test_e2e_x509_credentials.py new file mode 100644 index 0000000..144a75b --- /dev/null +++ b/packages/base/integration_tests/test_e2e_x509_credentials.py @@ -0,0 +1,52 @@ +from typing import List + +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client +from ai_api_client_sdk.models.scenario import Scenario +from . import (BASE_URL, CLIENT_ID, RESOURCE_GROUP_ID, X509_CERT_URL, X509_CERT_FILE_PATH, X509_KEY_FILE_PATH, + X509_CERT_STR, X509_KEY_STR) +from .ai_api_v2_client_e2e_test_base import AIAPIV2ClientE2ETestBase + + +class TestE2EX509Credentials(AIAPIV2ClientE2ETestBase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # write_x509_credentials_into_files() + cls.ai_api_v2_client = AIAPIV2Client(base_url=BASE_URL, auth_url=X509_CERT_URL, client_id=CLIENT_ID, + cert_file_path=X509_CERT_FILE_PATH, key_file_path=X509_KEY_FILE_PATH, + resource_group=RESOURCE_GROUP_ID) + + @classmethod + def tearDownClass(cls) -> None: + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # remove_x509_credentials() + super().tearDownClass() + + @staticmethod + def _get_scenario_from_scenarios(scenarios: List[Scenario], scenario_id: str): + for s in scenarios: + if s.id == scenario_id: + return s + return None + + def _query_and_assert_scenarios(self, client: AIAPIV2Client): + response = client.scenario.query() + scenarios = response.resources + self.assertEqual(response.count, len(scenarios)) + queried_scenario = self._get_scenario_from_scenarios(scenarios, self.test_scenario_id) + scenario = client.scenario.get(scenario_id=self.test_scenario_id) + self.assertEqual(queried_scenario, scenario) + self.assertIsNotNone(scenario.id) + self.assertIsNotNone(scenario.name) + self.assert_datetime(scenario.created_at) + self.assert_datetime(scenario.modified_at) + + def test_query_and_get_scenarios(self): + self._query_and_assert_scenarios(self.ai_api_v2_client) + + def test_with_x509_str(self): + client = AIAPIV2Client(base_url=BASE_URL, auth_url=X509_CERT_URL, client_id=CLIENT_ID, cert_str=X509_CERT_STR, + key_str=X509_KEY_STR, resource_group=RESOURCE_GROUP_ID) + self._query_and_assert_scenarios(client) diff --git a/packages/base/pyproject.toml b/packages/base/pyproject.toml new file mode 100644 index 0000000..e708884 --- /dev/null +++ b/packages/base/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "sap-ai-sdk-base" +version = "3.4.1" +description = "SAP Cloud SDK for AI (Python): Base Client" +readme = "PYPIDESCRIPTION.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [{ name = "SAP SE" }] +keywords = ["SAP AI Core"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.9" +dependencies = ["aenum~=3.1", "pyhumps~=3.0", "requests~=2.32"] + +[dependency-groups] +dev = [ + "pytest==9.0.3", + "pytest-cov==7.1.0", + "pylint==4.0.5", + "pytest-dotenv>=0.5.2", +] + +[tool.pytest.ini_options] +env_files = [".env"] + +[project.urls] +Homepage = "https://www.sap.com/" +Download = "https://pypi.python.org/pypi/sap-ai-sdk-base" + +[tool.setuptools.packages.find] +exclude = ["*test*"] + +[tool.commitizen] +name = "cz_customize" +tag_format = "base-v${version}" +ignored_tag_formats = ["*-v${version}"] +version_provider = "pep621" +changelog_file = "RELEASE_NOTES.md" + +[tool.commitizen.customize] +bump_pattern = '^(feat|fix)\(base\)' +changelog_pattern = '^(feat|fix)\(base\)' diff --git a/packages/base/sonar-project.properties b/packages/base/sonar-project.properties new file mode 100644 index 0000000..259c709 --- /dev/null +++ b/packages/base/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=ai-api-client-sdk +sonar.projectName=ai-api-client-sdk +sonar.projectVersion=3.4.0 +sonar.sources=./ai_api_client_sdk +sonar.exclusions=scripts/**,tests/** +sonar.dynamicAnalysis=reuseReports +sonar.core.codeCoveragePlugin=cobertura +sonar.python.coverage.reportPaths=coverage.xml +sonar.python.xunit.reportPath=unit_tests.xml +sonar.python.pylint.reportPath=pylint.log +sonar.qualitygate.wait=true diff --git a/packages/base/tests/__init__.py b/packages/base/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/tests/helpers/__init__.py b/packages/base/tests/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/tests/helpers/test_authenticator.py b/packages/base/tests/helpers/test_authenticator.py new file mode 100644 index 0000000..77efaaf --- /dev/null +++ b/packages/base/tests/helpers/test_authenticator.py @@ -0,0 +1,334 @@ +from typing import Tuple +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from ai_api_client_sdk.exception import AIAPIAuthenticatorException, AIAPIAuthenticatorInvalidRequestException, \ + AIAPIAuthenticatorAuthorizationException, AIAPIAuthenticatorServerException, \ + AIAPIAuthenticatorForbiddenException, AIAPIAuthenticatorMethodNotAllowedException, \ + AIAPIAuthenticatorTimeoutException +from ai_api_client_sdk.helpers.authenticator import Authenticator, PARAM_ERROR_MESSAGE + + +class ResponseMock: + def __init__(self, json: dict, status_code: int, text: str = ''): + self._json = json + self.status_code = status_code + self.text = text + + def json(self): + return self._json + + +class TestAuthenticator(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.auth_url = 'test_auth_url' + cls.client_id = 'test_client_id' + cls.client_secret = 'test_client_secret' + cls.cert_file_path = 'test_cert_file_path' + cls.key_file_path = 'test_key_file_path' + cls.cert_str = 'test_cert_str' + cls.key_str = 'test_key_str' + cls.token = 'test_token' + cls.token_expire_time = '43200' + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_authenticator(self, requests_mock): + response_mock = MagicMock() + response_mock.json.return_value = {'access_token': self.token, 'expires_in': self.token_expire_time} + response_mock.status_code = 200 + requests_mock.post.return_value = response_mock + data = {'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret} + cut = Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret) + generated_token = cut.get_token() + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + self.assertEqual(f'Bearer {self.token}', generated_token) + + def test_no_secret_no_cert_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_secret_cert_together_raises_exception(self): + # cert and key file paths + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + cert_file_path=self.cert_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + # cert and key strings + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + cert_str=self.cert_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + key_str=self.key_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret='test_secret', + cert_str=self.cert_str, key_str=self.key_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_x509_str_file_path_together_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str, + key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_file_path=self.cert_file_path, + key_str=self.key_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str, + key_str=self.key_str, key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str, + key_str=self.key_str, cert_file_path=self.cert_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, key_str=self.key_str, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_incomplete_x509_creds_raise_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, key_str=self.key_str) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_file_path=self.cert_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + Authenticator(auth_url=self.auth_url, client_id=self.client_id, key_file_path=self.key_file_path) + self.assertEqual(PARAM_ERROR_MESSAGE, cm.exception.error_message) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_with_x509_file_path(self, requests_mock): + requests_mock.post.return_value = ResponseMock( + {'access_token': self.token, 'expires_in': self.token_expire_time}, 200) + data = {'grant_type': 'client_credentials', 'client_id': self.client_id} + cut = Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_file_path=self.cert_file_path, + key_file_path=self.key_file_path) + generated_token = cut.get_token() + requests_mock.post.assert_called_with(url=self.auth_url, data=data, + cert=(self.cert_file_path, self.key_file_path)) + self.assertEqual(f'Bearer {self.token}', generated_token) + + def _requests_post_mock(self, url: str, data: dict, cert: Tuple[str, str]): + expected_data = {'grant_type': 'client_credentials', 'client_id': self.client_id} + self.assertEqual(self.auth_url, url) + self.assertEqual(expected_data, data) + cert_file_path, key_file_path = cert + with open(cert_file_path) as f: + self.assertEqual(self.cert_str, f.read()) + with open(key_file_path) as f: + self.assertEqual(self.key_str, f.read()) + return ResponseMock({'access_token': self.token, 'expires_in': self.token_expire_time}, 200) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_with_x509_str(self, requests_mock): + requests_mock.post = self._requests_post_mock + a = Authenticator(auth_url=self.auth_url, client_id=self.client_id, cert_str=self.cert_str, + key_str=self.key_str) + generated_token = a.get_token() + self.assertEqual(f'Bearer {self.token}', generated_token) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_error(self, requests_mock): + requests_mock.post.side_effect = Exception + a = Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret) + with self.assertRaises(AIAPIAuthenticatorException): + a.get_token() + data = {'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret} + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_invalid_request_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=400, + error_msg="Invalid request", + exception_class=AIAPIAuthenticatorInvalidRequestException, + expected_post_calls=1, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_unauthorized_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=401, + error_msg="Unauthorized", + exception_class=AIAPIAuthenticatorAuthorizationException, + expected_post_calls=1, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_forbidden_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=403, + error_msg="Forbidden", + exception_class=AIAPIAuthenticatorForbiddenException, + expected_post_calls=1, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_method_not_allowed_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=405, + error_msg="Method not allowed", + exception_class=AIAPIAuthenticatorMethodNotAllowedException, + expected_post_calls=1, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_timeout_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=408, + error_msg="Request timeout", + exception_class=AIAPIAuthenticatorTimeoutException, + expected_post_calls=4, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_server_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=500, + error_msg="Server error", + exception_class=AIAPIAuthenticatorServerException, + expected_post_calls=4, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_get_token_raises_exception(self, requests_mock): + self._do_test_get_token_raises_exception( + requests_mock=requests_mock, + status_code=200, + error_msg="Ok", + exception_class=AIAPIAuthenticatorException, + expected_post_calls=1, + ) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_token_is_cached(self, requests_mock): + token = 'test_token' + expires_in = '43200' # 12h + response_mock = MagicMock() + response_mock.json.return_value = {'access_token': token, 'expires_in': expires_in} + response_mock.status_code = 200 + requests_mock.post.return_value = response_mock + data = {'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret} + cut = Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret) + generated_token = cut.get_token() + generated_token = cut.get_token() + requests_mock.post.assert_called_once() + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + self.assertEqual(f'Bearer {token}', generated_token) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_token_is_valid_but_refresh(self, requests_mock): + token = 'test_token' + expires_in = '3600' # 1h + response_mock = MagicMock() + response_mock.json.return_value = {'access_token': token, 'expires_in': expires_in} + response_mock.status_code = 200 + requests_mock.post.return_value = response_mock + data = {'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret} + cut = Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret) + generated_token = cut.get_token() + generated_token = cut.get_token() + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + self.assertEqual(f'Bearer {token}', generated_token) + self.assertEqual(2, requests_mock.post.call_count) + + @patch('ai_api_client_sdk.helpers.authenticator.requests') + def test_token_is_expired(self, requests_mock): + token = 'test_token' + expires_in = '0' + response_mock = MagicMock() + response_mock.json.return_value = {'access_token': token, 'expires_in': expires_in} + response_mock.status_code = 200 + requests_mock.post.return_value = response_mock + data = {'grant_type': 'client_credentials', 'client_id': self.client_id, 'client_secret': self.client_secret} + cut = Authenticator(auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret) + generated_token = cut.get_token() + generated_token = cut.get_token() + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + self.assertEqual(f'Bearer {token}', generated_token) + self.assertEqual(2, requests_mock.post.call_count) + + def _do_test_get_token_raises_exception( + self, + requests_mock, + status_code, + error_msg, + exception_class, + expected_post_calls: int, + ): + response_mock = MagicMock() + response_mock.status_code = status_code + response_mock.text = error_msg + response_mock.json.side_effect = Exception + requests_mock.post.return_value = response_mock + + authenticator = Authenticator( + auth_url=self.auth_url, + client_id=self.client_id, + client_secret=self.client_secret, + ) + + with patch('ai_api_client_sdk.helpers.authenticator.time.sleep') as sleep_mock: + with self.assertRaises(exception_class) as exc_class: + authenticator.get_token() + + data = { + 'grant_type': 'client_credentials', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + } + + self.assertEqual(expected_post_calls, requests_mock.post.call_count) + requests_mock.post.assert_called_with(url=self.auth_url, data=data) + + if expected_post_calls == 1: + sleep_mock.assert_not_called() + else: + self.assertEqual(expected_post_calls - 1, sleep_mock.call_count) + + self.assertEqual('Could not retrieve Authorization token', exc_class.exception.description) + self.assertEqual(error_msg, exc_class.exception.error_message) + if status_code // 100 != 2: + self.assertEqual(exc_class.exception.status_code, status_code) + else: + self.assertEqual(exc_class.exception.status_code, 500) diff --git a/packages/base/tests/helpers/test_base_models.py b/packages/base/tests/helpers/test_base_models.py new file mode 100644 index 0000000..464694e --- /dev/null +++ b/packages/base/tests/helpers/test_base_models.py @@ -0,0 +1,48 @@ +from unittest import TestCase + +from ai_api_client_sdk.models.base_models import KeyValue, NameValue, Name, QueryResponse, BasicResponse + + +class TestBaseModels(TestCase): + def test_key_value_string_representation(self): + key = "dummy_key" + value = "dummy_value" + key_value_object = KeyValue(key=key, value=value) + self.assertIn("Key: ", key_value_object.__str__()) + self.assertIn(key, key_value_object.__str__()) + self.assertIn("Value: ", key_value_object.__str__()) + self.assertIn(value, key_value_object.__str__()) + + def test_name_value_string_representation(self): + name = "dummy_name" + value = "dummy_value" + name_value_object = NameValue(name=name, value=value) + self.assertIn("Name: ", name_value_object.__str__()) + self.assertIn(name, name_value_object.__str__()) + self.assertIn("Value: ", name_value_object.__str__()) + self.assertIn(value, name_value_object.__str__()) + + def test_name_string_representation(self): + name = "dummy_name" + name_object = Name(name=name) + self.assertIn("Name: ", name_object.__str__()) + self.assertIn(name, name_object.__str__()) + + def test_query_response_string_representation(self): + resources = ["dummy_resource1", "dummy_resource2"] + count = 1 + query_response_object = QueryResponse(resources=resources, count=count) + self.assertIn("Resources: ", query_response_object.__str__()) + for resource in resources: + self.assertIn(str(resource), query_response_object.__str__()) + self.assertIn("Count: ", query_response_object.__str__()) + self.assertIn(str(count), query_response_object.__str__()) + + def test_basic_response_string_representation(self): + dummy_id = "dummy_id" + message = "dummy_message" + name_object = BasicResponse(id=dummy_id, message=message) + self.assertIn("Id: ", name_object.__str__()) + self.assertIn(dummy_id, name_object.__str__()) + self.assertIn("Message: ", name_object.__str__()) + self.assertIn(message, name_object.__str__()) diff --git a/packages/base/tests/helpers/test_rest_client.py b/packages/base/tests/helpers/test_rest_client.py new file mode 100644 index 0000000..949f6f3 --- /dev/null +++ b/packages/base/tests/helpers/test_rest_client.py @@ -0,0 +1,458 @@ +import json +import os +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from ai_api_client_sdk.exception import AIAPIAuthorizationException, AIAPIInvalidRequestException, \ + AIAPINotFoundException, AIAPIPreconditionFailedException, AIAPIServerException +from ai_api_client_sdk.helpers.constants import DEBUG_ENV_VAR_NAME, SKIP_AUTH_ENV_VAR, Timeouts +from ai_api_client_sdk.helpers.rest_client import RestClient + +REQUESTS_PATCH_STRING = 'ai_api_client_sdk.helpers.rest_client.requests' + +class TestRestClient(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.base_url = 'test_base_url' + cls.resource_group = 'test_resource_group' + cls.client_type = 'test_client_type' + cls.path = '/test_path' + cls.url = f'{cls.base_url}{cls.path}' + cls.params = {'param1': 'value1'} + cls.body = {'key': 'value'} + cls.headers = {'AI-Resource-Group': cls.resource_group, 'Authorization': cls.get_token(), + 'AI-Client-Type': cls.client_type} + cls.response_json = {'response': 'OK'} + cls.response_mock = cls.create_response_mock(200, cls.response_json) + cls.rest_client = RestClient(base_url=cls.base_url, get_token=cls.get_token, resource_group=cls.resource_group, + client_type=cls.client_type) + + @staticmethod + def create_response_mock(status_code, json_dict, text=None): + response_mock = MagicMock() + response_mock.status_code = status_code + response_mock.json.return_value = json_dict + if text is None: + response_mock.text = json.dumps(json_dict) + else: + response_mock.text = text + return response_mock + + @staticmethod + def create_error_json(message=None, code=None, request_id=None, details=None): + error_json = { + 'error': { + 'message': message if message else 'Error message', + 'code': code if code else 'Error code', + 'requestId': request_id if request_id else 'request_id' + } + } + if details: + error_json['error']['details'] = details + return error_json + + def create_error_description(self, path=None): + path = path or self.path + return f'Failed to get {path}' + + def assert_server_exception(self, exception: AIAPIServerException, status_code: int, error_description: str = None, + error_json: dict = None, response_text: str = None): + self.assertEqual(status_code, exception.status_code) + error_description = error_description or self.create_error_description() + self.assertEqual(error_description, exception.description) + if response_text: + self.assertEqual(response_text, exception.error_message) + if error_json: + self.assertEqual(error_json['error']['message'], exception.error_message) + self.assertEqual(error_json['error']['code'], exception.error_code) + self.assertEqual(error_json['error']['requestId'], exception.request_id) + self.assertEqual(error_json['error'].get('details'), exception.details) + + @staticmethod + def get_token(): + return 'test_token' + + @patch(REQUESTS_PATCH_STRING) + def test_get(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.get(path=self.path, params=self.params) + request_session.get.assert_called_with(url=self.url, params=self.params, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_debug_log_api_call_enabled(self, requests_mock): + os.environ[DEBUG_ENV_VAR_NAME] = 'True' + request_session = MagicMock() + request_session.post.return_value = self.response_mock + requests_mock.Session.return_value = request_session + + with self.assertLogs(self.rest_client.logger, level='DEBUG') as cm: + r_json = self.rest_client.post(self.path, self.body, self.headers, self.resource_group) + self.assertEqual(self.response_json, r_json) + self.assertEqual(2, len(cm.output)) + self.assertTrue(self.resource_group in cm.output[0]) + self.assertTrue(self.path in cm.output[0]) + self.assertTrue(self.url in cm.output[0]) + self.assertTrue(str(self.body) in cm.output[0]) + token = self.get_token() + self.assertFalse(token in cm.output[0]) + for k, v in self.response_json.items(): + self.assertTrue(k in cm.output[1]) + self.assertTrue(v in cm.output[1]) + + @patch(REQUESTS_PATCH_STRING) + def test_debug_log_api_call_disabled(self, requests_mock): + os.environ[DEBUG_ENV_VAR_NAME] = 'False' + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + + error_msg = None + try: + with self.assertLogs(self.rest_client.logger, level='DEBUG') as cm: + self.rest_client._handle_request('get', self.path, self.params) + if len(cm.output) > 0: + error_msg = 'No logs should be generated when env var DEBUG is not set.' + except AssertionError: + # assertLogs raises AssertionError if no logs are generated + pass + + if error_msg: + self.fail(error_msg) + + @patch(REQUESTS_PATCH_STRING) + def test_get_empty_body(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(200, None, '') + request_session.get.return_value.json.side_effect = json.decoder.JSONDecodeError('msg', 'doc', 1) + requests_mock.Session.return_value = request_session + r_json = self.rest_client.get(path=self.path, params=self.params) + request_session.get.assert_called_with(url=self.url, params=self.params, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual('', r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_post(self, requests_mock): + request_session = MagicMock() + request_session.post.return_value = self.response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.post(path=self.path, body=self.body) + request_session.post.assert_called_with(url=self.url, params=None, json=self.body, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_post_empty_body(self, requests_mock): + request_session = MagicMock() + request_session.post.return_value = self.create_response_mock(200, None, '') + request_session.post.return_value.json.side_effect = json.decoder.JSONDecodeError('msg', 'doc', 1) + requests_mock.Session.return_value = request_session + r_json = self.rest_client.post(path=self.path, body=self.body) + request_session.post.assert_called_with(url=self.url, params=None, json=self.body, headers=self.headers, + timeout=(60, 60)) + self.assertEqual('', r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_patch(self, requests_mock): + request_session = MagicMock() + request_session.patch.return_value = self.response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.patch(path=self.path, body=self.body) + request_session.patch.assert_called_with(url=self.url, params=None, json=self.body, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_patch_empty_body(self, requests_mock): + request_session = MagicMock() + request_session.patch.return_value = self.create_response_mock(200, None, '') + request_session.patch.return_value.json.side_effect = json.decoder.JSONDecodeError('msg', 'doc', 1) + requests_mock.Session.return_value = request_session + r_json = self.rest_client.patch(path=self.path, body=self.body) + request_session.patch.assert_called_with(url=self.url, params=None, json=self.body, headers=self.headers, + timeout=(60, 60)) + self.assertEqual('', r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_delete(self, requests_mock): + request_session = MagicMock() + request_session.delete.return_value = self.response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.delete(path=self.path) + request_session.delete.assert_called_with(url=self.url, params=None, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_delete_empty_body(self, requests_mock): + request_session = MagicMock() + request_session.delete.return_value = self.create_response_mock(200, None, '') + request_session.delete.return_value.json.side_effect = json.decoder.JSONDecodeError('msg', 'doc', 1) + requests_mock.Session.return_value = request_session + r_json = self.rest_client.delete(path=self.path) + request_session.delete.assert_called_with(url=self.url, params=None, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual('', r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_client_type_from_parameter(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + self.rest_client.get(path=self.path) + headers = self.headers.copy() + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value)) + + @patch(REQUESTS_PATCH_STRING) + def test_client_type_from_env_var(self, requests_mock): + backup = os.environ.get('AI_CLIENT_TYPE', None) + try: + env_client_type = 'env_client_type' + os.environ['AI_CLIENT_TYPE'] = env_client_type + rest_client = RestClient(base_url=self.base_url, get_token=self.get_token, + resource_group=self.resource_group) + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + rest_client.get(path=self.path) + headers = self.headers.copy() + headers['AI-Client-Type'] = env_client_type + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value)) + finally: + if backup is not None: + os.environ['AI_CLIENT_TYPE'] = backup + else: + del os.environ['AI_CLIENT_TYPE'] + + @patch(REQUESTS_PATCH_STRING) + def test_client_type_env_var_precedence(self, requests_mock): + try: + env_client_type = 'env_client_type' + os.environ['AI_CLIENT_TYPE'] = env_client_type + rest_client = RestClient(base_url=self.base_url, get_token=self.get_token, + resource_group=self.resource_group, client_type='param_client_type') + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + rest_client.get(path=self.path) + headers = self.headers.copy() + headers['AI-Client-Type'] = env_client_type + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value)) + finally: + del os.environ['AI_CLIENT_TYPE'] + + @patch(REQUESTS_PATCH_STRING) + def test_no_client_type(self, requests_mock): + rest_client = RestClient(base_url=self.base_url, get_token=self.get_token, + resource_group=self.resource_group) + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + rest_client.get(path=self.path) + headers = self.headers.copy() + del headers['AI-Client-Type'] + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value)) + + @patch(REQUESTS_PATCH_STRING) + def test_get_with_resource_group(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + new_rg = 'new_resource_group' + headers = self.headers.copy() + headers['AI-Resource-Group'] = new_rg + r_json = self.rest_client.get(path=self.path, resource_group=new_rg) + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=headers, timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_camelize_decamelize(self, requests_mock): + body = {'body_key': 'body_value'} + c_body = {'bodyKey': 'body_value'} + params = {'param_key': 'param_value'} + c_params = {'paramKey': 'param_value'} + response_json = {'responseKey': 'response_value'} + d_response_json = {'response_key': 'response_value'} + request_session = MagicMock() + request_session.post.return_value = self.create_response_mock(200, response_json) + requests_mock.Session.return_value = request_session + r_json = self.rest_client.post(path=self.path, body=body) + request_session.post.assert_called_with(url=self.url, params=None, json=c_body, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(d_response_json, r_json) + request_session.get.return_value = self.create_response_mock(200, response_json) + r_json = self.rest_client.get(path=self.path, params=params) + request_session.get.assert_called_with(url=self.url, params=c_params, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(d_response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_not_camelize_body(self, requests_mock): + body = {'body_key': 'body_value'} + + response_json = {'responseKey': 'response_value'} + + request_session = MagicMock() + request_session.post.return_value = self.create_response_mock(200, response_json) + requests_mock.Session.return_value = request_session + + kwargs = {'convert_body_to_camel_case': False} + self.rest_client.post(path=self.path, body=body, **kwargs) + request_session.post.assert_called_with(url=self.url, params=None, json=body, headers=self.headers, + timeout=(60, 60)) + + @patch(REQUESTS_PATCH_STRING) + def test_not_camelize_params(self, requests_mock): + request_session = MagicMock() + params = {'param_key': 'param_value'} + kwargs = {'convert_params_to_camel_case': False} + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.get(path=self.path, params=params, **kwargs) + request_session.get.assert_called_with(url=self.url, params=params, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(self.response_json, r_json) + + @patch(REQUESTS_PATCH_STRING) + def test_authorization_exception(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(401, {}) + requests_mock.Session.return_value = request_session + with self.assertRaises(AIAPIAuthorizationException) as cm: + self.rest_client.get(path=self.path) + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=self.headers, + timeout=(60, 60)) + self.assertEqual(f'Failed to get {self.path}', cm.exception.description) + + @patch(REQUESTS_PATCH_STRING) + def test_request_exception(self, requests_mock): + request_session = MagicMock() + request_session.get.side_effect = Exception + requests_mock.Session.return_value = request_session + with self.assertRaises(Exception): + self.rest_client.get(path=self.path) + + @patch(REQUESTS_PATCH_STRING) + def test_server_exception(self, requests_mock): + status_code = 500 + response_text = 'error_text' + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(status_code, {}, response_text) + requests_mock.Session.return_value = request_session + with self.assertRaises(AIAPIServerException) as cm: + self.rest_client.get(path=self.path) + self.assert_server_exception(cm.exception, status_code, response_text=response_text) + + @patch(REQUESTS_PATCH_STRING) + def test_invalid_request_exception(self, requests_mock): + status_code = 400 + error_json = self.create_error_json(message='Invalid Request', details='Invalid') + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(status_code, error_json) + requests_mock.Session.return_value = request_session + with self.assertRaises(AIAPIInvalidRequestException) as cm: + self.rest_client.get(path=self.path) + self.assert_server_exception(cm.exception, status_code, error_json=error_json) + + @patch(REQUESTS_PATCH_STRING) + def test_not_found_exception(self, requests_mock): + status_code = 404 + error_json = self.create_error_json(message='Not Found') + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(status_code, error_json) + requests_mock.Session.return_value = request_session + with self.assertRaises(AIAPINotFoundException) as cm: + self.rest_client.get(path=self.path) + self.assert_server_exception(cm.exception, status_code, error_json=error_json) + + @patch(REQUESTS_PATCH_STRING) + def test_precondition_failed_exception(self, requests_mock): + status_code = 412 + error_json = self.create_error_json(message='Precondition Failed') + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(status_code, error_json) + requests_mock.Session.return_value = request_session + with self.assertRaises(AIAPIPreconditionFailedException) as cm: + self.rest_client.get(path=self.path) + self.assert_server_exception(cm.exception, status_code, error_json=error_json) + + @patch(REQUESTS_PATCH_STRING) + def test_kwargs(self, requests_mock): + request_session = MagicMock() + request_session.get.return_value = self.response_mock + request_session.post.return_value = self.response_mock + requests_mock.Session.return_value = request_session + + kwargs = {'a': True, 'b': 1, 'c': 'string'} + + rget_json = self.rest_client.get(path=self.path, params=self.params, headers=self.headers, **kwargs) + request_session.get.assert_called_with(url=self.url, params=self.params, json=None, headers=self.headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value), + **kwargs) + self.assertEqual(self.response_json, rget_json) + + rpost_json = self.rest_client.post(path=self.path, params=self.params, headers=self.headers, **kwargs) + request_session.post.assert_called_with(url=self.url, params=self.params, json=None, headers=self.headers, + timeout=(Timeouts.CONNECT_TIMEOUT.value, Timeouts.READ_TIMEOUT.value), + **kwargs) + self.assertEqual(self.response_json, rpost_json) + + @patch(REQUESTS_PATCH_STRING) + def test_kwargs_error(self, requests_mock): + status_code = 400 + error_json = self.create_error_json(message='Unexpected key', code=status_code) + request_session = MagicMock() + request_session.get.return_value = self.create_response_mock(status_code, error_json) + requests_mock.Session.return_value = request_session + kwargs = {'X': False} + with self.assertRaises(AIAPIServerException) as cm: + self.rest_client.get(path=self.path, **kwargs) + self.assert_server_exception(cm.exception, status_code, error_json=error_json) + + @patch(REQUESTS_PATCH_STRING) + def test_bytes_response(self, requests_mock): + response_mock = MagicMock() + response_mock.status_code = 200 + response_mock.content = b'response_bytes_content' + request_session = MagicMock() + request_session.get.return_value = response_mock + requests_mock.Session.return_value = request_session + r_json = self.rest_client.get(path=self.path, return_bytes_content=True) + request_session.get.assert_called_with(url=self.url, params=None, json=None, headers=self.headers, timeout=(60, 60)) + self.assertEqual(b'response_bytes_content', r_json) + + @patch('ai_api_client_sdk.helpers.rest_client.requests') + def test_skip_authorization_env_var(self, requests_mock): + # Set SKIP_AUTHORIZATION to 'true' + os.environ[SKIP_AUTH_ENV_VAR] = 'true' + request_session = MagicMock() + request_session.get.return_value = self.response_mock + requests_mock.Session.return_value = request_session + + # Remove Authorization from headers to simulate default behavior + headers = self.headers.copy() + headers.pop('Authorization', None) + + # Call get without Authorization header + r_json = self.rest_client.get(path=self.path, headers=headers) + # Ensure Authorization header is NOT set + called_headers = request_session.get.call_args[1]['headers'] + assert 'Authorization' not in called_headers + self.assertEqual(self.response_json, r_json) + + # Clean up + del os.environ[SKIP_AUTH_ENV_VAR] + + # Now test when SKIP_AUTHORIZATION is not set + request_session.get.reset_mock() + r_json = self.rest_client.get(path=self.path, headers=headers) + called_headers = request_session.get.call_args[1]['headers'] + assert 'Authorization' in called_headers + self.assertEqual(self.response_json, r_json) diff --git a/packages/base/tests/resource_clients/__init__.py b/packages/base/tests/resource_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/base/tests/resource_clients/resource_client_test_base.py b/packages/base/tests/resource_clients/resource_client_test_base.py new file mode 100644 index 0000000..fe73d21 --- /dev/null +++ b/packages/base/tests/resource_clients/resource_client_test_base.py @@ -0,0 +1,67 @@ +from typing import Any, Callable, Dict, List, Union +from unittest import TestCase +from unittest.mock import MagicMock + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.enactment import Enactment +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus + + +class ResourceClientTestBase(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.resource_group = 'test_resource_group' + + def setUp(self): + super().setUp() + self.rest_client_mock = MagicMock() + self.client = None + + def assert_object(self, d: Dict[str, Any], o: object): + for k in d.keys(): + self.assertEqual(d[k], getattr(o, k)) + + def assert_enactment(self, e_dict: dict, e: Enactment): + e_dict['created_at'] = parse_datetime(e_dict['created_at']) + e_dict['modified_at'] = parse_datetime(e_dict['modified_at']) + e_dict['status'] = Status(e_dict['status']) + e_dict['target_status'] = TargetStatus(e_dict['target_status']) + e_dict['submission_time'] = parse_datetime(e_dict['submission_time']) + e_dict['start_time'] = parse_datetime(e_dict['start_time']) + e_dict['completion_time'] = parse_datetime(e_dict['completion_time']) + self.assert_object(e_dict, e) + + def assert_object_lists(self, dict_list: List[Dict[str, Any]], object_list: list, + assert_object_function: Callable = None, sort_key: str = 'id'): + if not assert_object_function: + assert_object_function = self.assert_object + self.assertEqual(len(dict_list), len(object_list)) + dict_list = sorted(dict_list, key=lambda ad: ad[sort_key]) + object_list = sorted(object_list, key=lambda a: getattr(a, sort_key)) + for i in range(len(object_list)): + assert_object_function(dict_list[i], object_list[i]) + + def assert_count(self, path: str, count: int, params: Union[Dict[str, Any], None] = None): + params = params + self.rest_client_mock.get.return_value = count + if params: + res_count = self.client.count(resource_group=self.resource_group, **params) + if 'executable_ids' in params: + params['executable_ids'] = ','.join(params['executable_ids']) + if 'status' in params: + params['status'] = params['status'].value + if 'kind' in params: + params['kind'] = params['kind'].value + if 'search' in params: + params['$search'] = params['search'] + del params['search'] + if 'expand' in params: + params['$expand'] = params['expand'] + del params['expand'] + else: + res_count = self.client.count(resource_group=self.resource_group) + + self.rest_client_mock.get.assert_called_with(path=path, params=params, + resource_group=self.resource_group) + self.assertEqual(res_count, count) diff --git a/packages/base/tests/resource_clients/test_artifact_client.py b/packages/base/tests/resource_clients/test_artifact_client.py new file mode 100644 index 0000000..1c4cf12 --- /dev/null +++ b/packages/base/tests/resource_clients/test_artifact_client.py @@ -0,0 +1,226 @@ +import uuid +from copy import deepcopy + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.scenario import Scenario +from ai_api_client_sdk.resource_clients.artifact_client import ArtifactClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestArtifactClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ArtifactClient(self.rest_client_mock) + + @staticmethod + def create_artifact_dict(labels=None): + if labels is None: + labels = [ + {'key': 'ext.ai.sap.com/s4hana-version', 'value': 'string'}] + return { + "labels": labels, + "name": "string", + "kind": "model", + "url": "https://example.com/some_path", + "description": "string", + "id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "execution_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + + @staticmethod + def create_artifact_with_expand_scenario_dict(labels=None): + if labels is None: + labels = [ + {'key': 'ext.ai.sap.com/s4hana-version', 'value': 'string'}] + return { + "labels": labels, + "name": "string", + "kind": "model", + "url": "https://example.com/some_path", + "description": "string", + "id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "execution_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z", + "scenario": { + "created_at": "2021-03-29T12:57:05Z", + "description": "ML API Facade Test Scenario description", + "id": "88888888-4444-4444-4444-cccccccccccc", + "labels": [ + { + "key": "ext.ai.sap.com/xyz", + "value": "string" + } + ], + "modified_at": "2021-03-29T12:57:05Z", + "name": "ML-API-Facade-Test-Scenario" + } + } + + def assert_artifact(self, artifact_dict: dict, artifact: Artifact): + artifact_dict['created_at'] = parse_datetime(artifact_dict['created_at']) + artifact_dict['modified_at'] = parse_datetime(artifact_dict['modified_at']) + artifact_dict['kind'] = Artifact.Kind(artifact_dict['kind']) + artifact_dict['labels'] = [Label.from_dict(l) for l in artifact_dict['labels']] + if artifact_dict.get('scenario'): + artifact_dict['scenario'] = Scenario.from_dict(artifact_dict['scenario']) + self.assert_object(artifact_dict, artifact) + + def test_get_artifact(self): + artifact_dict = self.create_artifact_dict() + self.rest_client_mock.get.return_value = artifact_dict.copy() + a = self.client.get(artifact_id=artifact_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/artifacts/{artifact_dict["id"]}', params=None, + resource_group=self.resource_group) + self.assert_artifact(artifact_dict, a) + self.assertIn("Artifact id: ", a.__str__()) + self.assertIn(artifact_dict['id'], a.__str__()) + self.assertIn("Label key: ", artifact_dict['labels'][0].__str__()) + self.assertIn(artifact_dict['labels'][0].key, artifact_dict['labels'][0].__str__()) + self.assertIn("Label value: ", artifact_dict['labels'][0].__str__()) + self.assertIn(artifact_dict['labels'][0].value, artifact_dict['labels'][0].__str__()) + + def test_get_artifact_with_expand_scenario(self): + artifact_with_expanded_scenario_dict = self.create_artifact_with_expand_scenario_dict() + self.rest_client_mock.get.return_value = deepcopy(artifact_with_expanded_scenario_dict) + response = self.client.get(artifact_id=artifact_with_expanded_scenario_dict['id'], expand='scenario', + resource_group=self.resource_group) + params = {'$expand': 'scenario'} + self.rest_client_mock.get.assert_called_with(path=f'/artifacts/{artifact_with_expanded_scenario_dict["id"]}', + params=params, + resource_group=self.resource_group) + self.assert_artifact(artifact_with_expanded_scenario_dict, response) + + def test_query_artifacts(self): + n = 3 + artifacts = [self.create_artifact_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [a.copy() for a in artifacts], 'count': n} + aqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=None, resource_group=None) + self.assert_object_lists(artifacts, aqr.resources, self.assert_artifact) + self.assertIn("Resources: ", aqr.__str__()) + for artifact in artifacts: + self.assertIn(artifact['id'], aqr.__str__()) + self.assertIn("Count: " + str(len(artifacts)), aqr.__str__()) + + params = {'scenario_id': 'test_scenario_id', 'execution_id': 'test_execution_id', 'name': 'test_name', + 'kind': Artifact.Kind.MODEL, 'top': 5, 'skip': 1, 'search': 'test_search'} # type:dict + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['kind'] = params['kind'].value + params['$search'] = params['search'] + del params['search'] + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=params, + resource_group=self.resource_group) + + def test_query_artifacts_search_case_insensitive(self): + params = {'scenario_id': 'test_scenario_id', 'execution_id': 'test_execution_id', 'name': 'test_name', + 'kind': Artifact.Kind.MODEL, 'top': 5, 'skip': 1, 'search': 'test_search', + 'search_case_insensitive': True} # type:dict + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['kind'] = params['kind'].value + params['$search'] = params['search'] + del params['search'] + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=params, + resource_group=self.resource_group) + + def test_query_artifacts_with_expand_scenario(self): + n = 3 + artifacts = [self.create_artifact_with_expand_scenario_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [deepcopy(a) for a in artifacts], 'count': n} + aqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=None, resource_group=None) + self.assert_object_lists(artifacts, aqr.resources, self.assert_artifact) + + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + params = {'scenario_id': 'test_scenario_id', 'execution_id': 'test_execution_id', 'name': 'test_name', + 'kind': Artifact.Kind.MODEL, 'top': 5, 'skip': 1, 'search': 'test_search', + 'expand': 'scenario'} # type:dict + self.client.query(resource_group=self.resource_group, **params) + params['kind'] = params['kind'].value + params['$search'] = params['search'] + del params['search'] + params['$expand'] = params['expand'] + del params['expand'] + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=params, + resource_group=self.resource_group) + + def test_filter_artifacts_by_labels(self): + labels = [] + artifacts = [] + labels.append({'key': 'ext.ai.sap.com/s4hana-version', 'value': 'test'}) + artifacts.append(self.create_artifact_dict(labels=[labels[0]])) + labels.append({'key': 'ext.ai.sap.com/s4hana-version', 'value': 'test2'}) + artifacts.append(self.create_artifact_dict(labels=[labels[1]])) + artifacts.append(self.create_artifact_dict(labels=labels)) + self.rest_client_mock.get.return_value = {'resources': [a.copy() for a in artifacts], 'count': 3} + ac = ArtifactClient(self.rest_client_mock) + + params = {'artifact_label_selector': 'ext.ai.sap.com/s4hana-version!=test'} + self.rest_client_mock.get.return_value = {'resources': [artifacts[0]], 'count': 1} + ac.query(resource_group=self.resource_group, **params) + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=params, + resource_group=self.resource_group) + + params = {'artifact_label_selector': 'ext.ai.sap.com/s4hana-version=test, ext.ai.sap.com/dummy=dummy'} + self.rest_client_mock.get.return_value = {'resources': [artifacts[2]], 'count': 1} + ac.query(resource_group=self.resource_group, **params) + self.rest_client_mock.get.assert_called_with(path='/artifacts', params=params, + resource_group=self.resource_group) + + def test_create_artifact(self): + artifact_dict = self.create_artifact_dict() + response_message = 'Artifact created' + self.rest_client_mock.post.return_value = {'id': artifact_dict['id'], 'message': response_message, + 'url': artifact_dict['url']} + acr = self.client.create(name=artifact_dict['name'], kind=Artifact.Kind(artifact_dict['kind']), + url=artifact_dict['url'], + scenario_id=artifact_dict['scenario_id'], + description=artifact_dict['description'], + labels=[Label.from_dict(l) for l in artifact_dict['labels']], + resource_group=self.resource_group) + body = artifact_dict.copy() + del body['id'] + del body['execution_id'] + del body['created_at'] + del body['modified_at'] + self.rest_client_mock.post.assert_called_with(path='/artifacts', body=body, resource_group=self.resource_group) + self.assertEqual(artifact_dict['id'], acr.id) + self.assertEqual(response_message, acr.message) + self.assertEqual(artifact_dict['url'], acr.url) + + def test_artifact_kind_other(self): + # Make sure artifact of type `other` can be created + artifact_dict = self.create_artifact_dict() + artifact_dict['kind'] = 'other' + self.rest_client_mock.post.return_value = {'id': artifact_dict['id'], + 'message': 'Artifact of type `other` created', + 'url': artifact_dict['url']} + acr = self.client.create(name=artifact_dict['name'], kind=Artifact.Kind(artifact_dict['kind']), + url=artifact_dict['url'], + scenario_id=artifact_dict['scenario_id'], + description=artifact_dict['description'], + labels=[Label.from_dict(l) for l in artifact_dict['labels']], + resource_group=self.resource_group) + + self.rest_client_mock.get.return_value = artifact_dict.copy() + response = self.client.get(acr.id, resource_group=self.resource_group) + self.assertEqual(response.kind, Artifact.Kind.OTHER) + + def test_count_artifacts(self): + self.assert_count('/artifacts/$count', 12) + self.assert_count('/artifacts/$count', 10, {'scenario_id': 'test_scenario_id'}) + self.assert_count('/artifacts/$count', 9, {'execution_id': 'test_execution_id'}) + self.assert_count('/artifacts/$count', 1, {'name': 'test_name'}) + self.assert_count('/artifacts/$count', 9, {'kind': Artifact.Kind.MODEL}) + self.assert_count('/artifacts/$count', 9, {'artifact_label_selector': 'ext.ai.sap.com/dummy!=dummy'}) + self.assert_count('/artifacts/$count', 1, + {'scenario_id': 'test_scenario_id', 'execution_id': 'test_execution_id', 'name': 'test_name', + 'kind': Artifact.Kind.MODEL, 'artifact_label_selector': 'ext.ai.sap.com/dummy!=dummy'}) diff --git a/packages/base/tests/resource_clients/test_configuration_client.py b/packages/base/tests/resource_clients/test_configuration_client.py new file mode 100644 index 0000000..8049a04 --- /dev/null +++ b/packages/base/tests/resource_clients/test_configuration_client.py @@ -0,0 +1,184 @@ +import uuid +from copy import deepcopy + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.configuration import Configuration +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.models.scenario import Scenario +from ai_api_client_sdk.resource_clients.configuration_client import ConfigurationClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestConfigurationClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ConfigurationClient(self.rest_client_mock) + + @staticmethod + def create_configuration_dict(): + return { + "id": str(uuid.uuid4()), + "name": "configuration_name", + "executable_id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "parameter_bindings": [ + { + "key": "param_key", + "value": "param_value" + } + ], + "input_artifact_bindings": [ + { + "key": "input_artifact", + "artifact_id": str(uuid.uuid4()) + } + ], + "created_at": "2021-03-29T12:58:05Z" + } + + @staticmethod + def create_configuration_dict_with_expand_scenario_dict(): + return { + "id": str(uuid.uuid4()), + "name": "configuration_name", + "executable_id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "parameter_bindings": [ + { + "key": "param_key", + "value": "param_value" + } + ], + "input_artifact_bindings": [ + { + "key": "input_artifact", + "artifact_id": str(uuid.uuid4()) + } + ], + "created_at": "2021-03-29T12:58:05Z", + "scenario": { + "created_at": "2021-03-29T12:57:05Z", + "description": "ML API Facade Test Scenario description", + "id": "88888888-4444-4444-4444-cccccccccccc", + "labels": [ + { + "key": "ext.ai.sap.com/xyz", + "value": "string" + } + ], + "modified_at": "2021-03-29T12:57:05Z", + "name": "ML-API-Facade-Test-Scenario" + } + } + + def assert_configuration(self, conf_dict: dict, conf: Configuration): + conf_dict['parameter_bindings'] = [ParameterBinding.from_dict(pb) for pb in conf_dict['parameter_bindings']] + conf_dict['input_artifact_bindings'] = [InputArtifactBinding.from_dict(iab) + for iab in conf_dict['input_artifact_bindings']] + conf_dict['created_at'] = parse_datetime(conf_dict['created_at']) + if conf_dict.get('scenario'): + conf_dict['scenario'] = Scenario.from_dict(conf_dict['scenario']) + self.assert_object(conf_dict, conf) + + def test_get_configuration(self): + conf_dict = self.create_configuration_dict() + self.rest_client_mock.get.return_value = conf_dict.copy() + conf = self.client.get(configuration_id=conf_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/configurations/{conf_dict["id"]}', params=None, + resource_group=self.resource_group) + self.assert_configuration(conf_dict, conf) + self.assertIn("Configuration id: ", conf.__str__()) + self.assertIn(conf_dict['id'], conf.__str__()) + + def test_get_configuration_with_expand_scenario(self): + conf_with_expanded_scenario_dict = self.create_configuration_dict_with_expand_scenario_dict() + self.rest_client_mock.get.return_value = deepcopy(conf_with_expanded_scenario_dict) + response = self.client.get(configuration_id=conf_with_expanded_scenario_dict['id'], expand='scenario', + resource_group=self.resource_group) + params = {'$expand': 'scenario'} + self.rest_client_mock.get.assert_called_with(path=f'/configurations/{conf_with_expanded_scenario_dict["id"]}', + params=params, + resource_group=self.resource_group) + self.assert_configuration(conf_with_expanded_scenario_dict, response) + + def test_query_configurations(self): + n = 3 + conf_dicts = [self.create_configuration_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [cd.copy() for cd in conf_dicts], 'count': n} + cqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/configurations', params=None, resource_group=None) + self.assert_object_lists(conf_dicts, cqr.resources, self.assert_configuration) + + params = {'scenario_id': 'test_scenario_id', 'executable_ids': ['test_executable_id'], 'top': 5, 'skip': 1, + 'search': 'test_search'} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['executable_ids'] = ','.join(params['executable_ids']) + params['$search'] = params['search'] + del params['search'] + self.rest_client_mock.get.assert_called_with(path='/configurations', params=params, + resource_group=self.resource_group) + + def test_query_configurations_search_case_insensitive(self): + params = {'scenario_id': 'test_scenario_id', 'executable_ids': ['test_executable_id'], 'top': 5, 'skip': 1, + 'search': 'test_search', 'search_case_insensitive': True} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['executable_ids'] = ','.join(params['executable_ids']) + params['$search'] = params['search'] + del params['search'] + self.rest_client_mock.get.assert_called_with(path='/configurations', params=params, + resource_group=self.resource_group) + + def test_query_configurations_with_expand_scenario(self): + n = 3 + conf_dicts = [self.create_configuration_dict_with_expand_scenario_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [deepcopy(cd) for cd in conf_dicts], 'count': n} + cqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/configurations', params=None, resource_group=None) + self.assert_object_lists(conf_dicts, cqr.resources, self.assert_configuration) + + params = {'scenario_id': 'test_scenario_id', 'executable_ids': ['test_executable_id'], 'top': 5, 'skip': 1, + 'search': 'test_search', 'expand': 'scenario'} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['executable_ids'] = ','.join(params['executable_ids']) + params['$search'] = params['search'] + del params['search'] + params['$expand'] = params['expand'] + del params['expand'] + self.rest_client_mock.get.assert_called_with(path='/configurations', params=params, + resource_group=self.resource_group) + + def test_create_configuration(self): + conf_dict = self.create_configuration_dict() + response_message = 'Configuration created' + self.rest_client_mock.post.return_value = {'id': conf_dict['id'], 'message': response_message} + parameter_bindings = [ParameterBinding.from_dict(pb) for pb in conf_dict['parameter_bindings']] + ccr = self.client.create(name=conf_dict['name'], scenario_id=conf_dict['scenario_id'], + executable_id=conf_dict['executable_id'], + parameter_bindings=parameter_bindings, + input_artifact_bindings=[InputArtifactBinding.from_dict(iab) for + iab in conf_dict['input_artifact_bindings']], + resource_group=self.resource_group) + body = conf_dict.copy() + del body['id'] + del body['created_at'] + self.rest_client_mock.post.assert_called_with(path='/configurations', body=body, + resource_group=self.resource_group) + self.assertEqual(conf_dict['id'], ccr.id) + self.assertEqual(response_message, ccr.message) + self.assertIn("Parameter binding key: ", parameter_bindings[0].__str__()) + self.assertIn(parameter_bindings[0].key, parameter_bindings[0].__str__()) + self.assertIn("Parameter binding value: ", parameter_bindings[0].__str__()) + self.assertIn(parameter_bindings[0].value, parameter_bindings[0].__str__()) + + def test_count_configurations(self): + self.assert_count('/configurations/$count', 3) + self.assert_count('/configurations/$count', 0, {'scenario_id': 'test_scenario_id'}) + self.assert_count('/configurations/$count', 1, {'executable_ids': ['test_executable_id']}) + self.assert_count('/configurations/$count', 1, + {'scenario_id': 'test_scenario_id', 'executable_ids': ['test_executable_id']}) + self.assert_count('/configurations/$count', 2, + {'scenario_id': 'test_scenario_id', 'search': 'test_search'}) diff --git a/packages/base/tests/resource_clients/test_deployment_client.py b/packages/base/tests/resource_clients/test_deployment_client.py new file mode 100644 index 0000000..5c9523d --- /dev/null +++ b/packages/base/tests/resource_clients/test_deployment_client.py @@ -0,0 +1,252 @@ +import copy +import uuid +from datetime import datetime + +from ai_api_client_sdk.exception import AIAPIInvalidInputException +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT, parse_datetime +from ai_api_client_sdk.models.base_models import Operation, Order, BasicModifyRequest +from ai_api_client_sdk.models.deployment import Deployment +from ai_api_client_sdk.models.deployment_bulk_modify_response import DeploymentBulkModifyResponse +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from ai_api_client_sdk.resource_clients.deployment_client import DeploymentClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestDeploymentClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = DeploymentClient(self.rest_client_mock) + + def assert_deployment(self, d_dict: dict, d: Deployment): + if d_dict.get('last_operation'): + d_dict['last_operation'] = Operation(d_dict['last_operation']) + self.assert_enactment(d_dict, d) + + @staticmethod + def create_deployment_dict(): + return { + "id": str(uuid.uuid4()), + "deployment_url": "test_deployment_url", + "configuration_id": str(uuid.uuid4()), + "configuration_name": "test_configuration_name", + "scenario_id": str(uuid.uuid4()), + "status": "RUNNING", + "target_status": "RUNNING", + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z", + "submission_time": "2021-03-29T12:58:05Z", + "start_time": "2021-03-29T12:58:05Z", + "completion_time": "2021-03-29T12:58:05Z", + "status_message": "Deployment running", + "status_details": {}, + "details": {}, + "ttl": "10H" + } + + def test_get_deployment(self): + dep_dict = self.create_deployment_dict() + self.rest_client_mock.get.return_value = dep_dict.copy() + dep = self.client.get(deployment_id=dep_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/deployments/{dep_dict["id"]}', + resource_group=self.resource_group) + self.assert_deployment(dep_dict, dep) + self.assertIn("Deployment id: ", dep.__str__()) + self.assertIn(dep_dict['id'], dep.__str__()) + self.assertEqual(dep.status_message, dep_dict['status_message']) + self.assertEqual(dep.ttl, dep_dict['ttl']) + + def test_deployment_get_status(self): + response_dict = { + "configuration_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "id": str(uuid.uuid4()), + "modified_at": "2021-03-29T12:58:05Z", + "status": "RUNNING", + "status_details": {}, + "details": {} + } + self.rest_client_mock.get.return_value = response_dict.copy() + dep = self.client.get(deployment_id=response_dict['id'], resource_group=self.resource_group, select='status') + self.rest_client_mock.get.assert_called_with(path=f'/deployments/{response_dict["id"]}', + params={'$select': 'status'}, + resource_group=self.resource_group) + self.assertEqual(dep.status, Status(response_dict['status'])) + self.assertEqual(dep.status_details, response_dict['status_details']) + self.assertEqual(dep.details, response_dict['details']) + + def test_get_deployment_with_last_operation(self): + dep_dict = self.create_deployment_dict() + dep_dict['last_operation'] = Operation.UPDATE.value + dep_dict['latest_running_configuration_id'] = str(uuid.uuid4()) + self.rest_client_mock.get.return_value = dep_dict.copy() + dep = self.client.get(deployment_id=dep_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/deployments/{dep_dict["id"]}', + resource_group=self.resource_group) + self.assert_deployment(dep_dict, dep) + + def test_get_deployment_with_last_operation_new_value(self): + dep_dict = self.create_deployment_dict() + dep_dict['last_operation'] = 'DUMMY-OPERATION' + dep_dict['latest_running_configuration_id'] = str(uuid.uuid4()) + self.rest_client_mock.get.return_value = dep_dict.copy() + dep = self.client.get(deployment_id=dep_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/deployments/{dep_dict["id"]}', + resource_group=self.resource_group) + self.assert_deployment(dep_dict, dep) + + def test_query_deployments(self): + n = 3 + dep_dicts = [self.create_deployment_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [dd.copy() for dd in dep_dicts], 'count': n} + dqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/deployments', params=None, resource_group=None) + self.assert_object_lists(dep_dicts, dqr.resources, self.assert_enactment) + + params = {'scenario_id': 'test_scenario_id', 'configuration_id': 'test_configuration_id', + 'executable_ids': ['test_executable_id'], 'status': Status.RUNNING, 'top': 5, 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['executable_ids'] = ','.join(params['executable_ids']) + params['status'] = params['status'].value + self.rest_client_mock.get.assert_called_with(path='/deployments', params=params, + resource_group=self.resource_group) + + def test_create_deployment(self): + dep_dict = self.create_deployment_dict() + response_message = 'Deployment created' + self.rest_client_mock.post.return_value = {'id': dep_dict['id'], 'message': response_message, + 'deployment_url': dep_dict['deployment_url'], + 'status': dep_dict['status']} + dcr = self.client.create(configuration_id=dep_dict['configuration_id'], resource_group=self.resource_group) + body = {"configuration_id": dep_dict['configuration_id']} + self.rest_client_mock.post.assert_called_with(path='/deployments', body=body, + resource_group=self.resource_group) + self.assertEqual(dep_dict['id'], dcr.id) + self.assertEqual(response_message, dcr.message) + self.assertEqual(dep_dict['deployment_url'], dcr.deployment_url) + self.assertEqual(Status(dep_dict['status']), dcr.status) + + def test_create_deployment_with_ttl(self): + dep_dict = self.create_deployment_dict() + response_message = 'Deployment created' + self.rest_client_mock.post.return_value = {'id': dep_dict['id'], 'message': response_message, + 'deployment_url': dep_dict['deployment_url'], + 'status': dep_dict['status'], + 'ttl': dep_dict['ttl']} + dcr = self.client.create(configuration_id=dep_dict['configuration_id'], ttl=dep_dict['ttl'], + resource_group=self.resource_group) + body = {"configuration_id": dep_dict['configuration_id'], "ttl": dep_dict['ttl']} + self.rest_client_mock.post.assert_called_with(path='/deployments', body=body, + resource_group=self.resource_group) + self.assertEqual(dep_dict['id'], dcr.id) + self.assertEqual(response_message, dcr.message) + self.assertEqual(dep_dict['deployment_url'], dcr.deployment_url) + self.assertEqual(Status(dep_dict['status']), dcr.status) + self.assertEqual(dep_dict['ttl'], dcr.ttl) + + def test_modify_deployment_with_target_status(self): + response_dict = {'id': 'test_deployment_id', 'message': 'Deployment patched'} + body = {'target_status': TargetStatus.STOPPED} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(deployment_id=response_dict['id'], resource_group=self.resource_group, **body) + body['target_status'] = body['target_status'].value + self.rest_client_mock.patch.assert_called_with(path=f'/deployments/{response_dict["id"]}', body=body, + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_bulk_modify_deployments(self): + deployments = [ + BasicModifyRequest("deployment_1", TargetStatus.STOPPED), + BasicModifyRequest("deployment_2", TargetStatus.DELETED) + ] + body = {'deployments': [dmr.to_dict() for dmr in deployments]} + + response_dict = { + "deployments": [ + { + "id": "deployment_1", + "message": "Deployment modification scheduled" + }, + { + "id": "deployment_2", + "error": { + "code": "string", + "message": "string", + "request_id": "string", + "target": "string", + "details": {} + } + } + ] + } + self.rest_client_mock.patch.return_value = copy.deepcopy(response_dict) + dbmr = self.client.bulk_modify(deployments=deployments, resource_group=self.resource_group) + headers = {'Content-Type': 'application/merge-patch+json'} + self.rest_client_mock.patch.assert_called_with(path=f'/deployments', body=body, headers=headers, + resource_group=self.resource_group) + + self.assertIsInstance(dbmr, DeploymentBulkModifyResponse) + self.assertEqual(response_dict['deployments'][0]['id'], dbmr.deployments[0].id) + self.assertEqual(response_dict['deployments'][0]['message'], dbmr.deployments[0].message) + self.assertEqual(response_dict['deployments'][1]['id'], dbmr.deployments[1].id) + self.assertEqual(response_dict['deployments'][1]['error']['code'], dbmr.deployments[1].error.code) + dbmr_str = dbmr.__str__() + self.assertIn(response_dict['deployments'][0]['id'], dbmr_str) + self.assertIn(response_dict['deployments'][1]['id'], dbmr_str) + + def test_modify_deployment_with_configuration_id(self): + response_dict = {'id': 'test_deployment_id', 'message': 'Deployment patched'} + body = {'configuration_id': 'test_configuration_id'} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(deployment_id=response_dict['id'], resource_group=self.resource_group, **body) + self.rest_client_mock.patch.assert_called_with(path=f'/deployments/{response_dict["id"]}', body=body, + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_modify_deployment_with_both_target_status_and_conf_id_fails(self): + with self.assertRaises(AIAPIInvalidInputException): + self.client.modify(deployment_id='test_id', target_status=TargetStatus.STOPPED, + configuration_id='test_conf_id') + + def test_delete_deployment(self): + response_dict = {'id': 'test_deployment_id', 'message': 'Deployment deleted'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(deployment_id=response_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.delete.assert_called_with(f'/deployments/{response_dict["id"]}', + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_count_deployments(self): + self.assert_count('/deployments/$count', 7) + self.assert_count('/deployments/$count', 6, {'scenario_id': 'test_scenario_id'}) + self.assert_count('/deployments/$count', 5, {'configuration_id': 'test_configuration_id'}) + self.assert_count('/deployments/$count', 6, {'executable_ids': ['test_executable_id']}) + self.assert_count('/deployments/$count', 3, {'status': Status.RUNNING}) + self.assert_count('/deployments/$count', 0, + {'scenario_id': 'test_scenario_id', 'configuration_id': 'test_configuration_id', + 'executable_ids': ['test_executable_id'], 'status': Status.RUNNING}) + + def test_logs(self): + deployment_id = 'test_deployment_id' + top = 5 + start = datetime.utcnow() + end = datetime.utcnow() + order = Order.DESC + params = {'top': top, 'start': start.strftime(DATETIME_FORMAT), 'end': end.strftime(DATETIME_FORMAT), + 'order': order.value} + response_dict = {'data': {'result': [{'msg': 'log message', + 'timestamp': '2021-08-27T15:10:36.774534098+00:00'}]}} + self.rest_client_mock.get.return_value = copy.deepcopy(response_dict) + lr = self.client.query_logs(deployment_id=deployment_id, top=top, start=start, end=end, order=order, + resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/deployments/{deployment_id}/logs', params=params, + resource_group=self.resource_group) + self.assertEqual(len(response_dict['data']['result']), len(lr.data.result)) + self.assertEqual(response_dict['data']['result'][0]['msg'], lr.data.result[0].msg) + self.assertEqual(parse_datetime(response_dict['data']['result'][0]['timestamp']), lr.data.result[0].timestamp) + self.assertIn("Log response messages:", lr.__str__()) + self.assertIn(response_dict['data']['result'][0]['msg'], lr.__str__()) diff --git a/packages/base/tests/resource_clients/test_executable_client.py b/packages/base/tests/resource_clients/test_executable_client.py new file mode 100644 index 0000000..e880f81 --- /dev/null +++ b/packages/base/tests/resource_clients/test_executable_client.py @@ -0,0 +1,109 @@ +import copy +import uuid +from unittest.mock import MagicMock + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.executable import Executable +from ai_api_client_sdk.models.input_artifact import InputArtifact +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.output_artifact import OutputArtifact +from ai_api_client_sdk.models.parameter import Parameter +from ai_api_client_sdk.resource_clients.executable_client import ExecutableClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestExecutableClient(ResourceClientTestBase): + @staticmethod + def create_executable_dict(): + return { + "labels": [ + { + "key": "ext.ai.sap.com/s4hana-version", + "value": "label_value" + } + ], + "name": "test_name", + "description": "test_description", + "id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "version_id": "test_version_id", + "parameters": [ + { + "name": "test_param_name", + "type": "string", + "default": "test", + "description": "test description" + + } + ], + "input_artifacts": [ + { + "name": "test_input_artifact_name", + "kind": "dataset", + "description": "artifact description", + "labels": [{'key': 'ext.ai.sap.com/customkey1', 'value': 'customvalue1'}, + {'key': 'ext.ai.sap.com/customkey2', 'value': 'customvalue2'}] + } + ], + "output_artifacts": [ + { + "name": "test_output_artifact_name", + "kind": "model", + "description": "artifact description", + "labels": [{'key': 'ext.ai.sap.com/customkey1', 'value': 'customvalue1'}, + {'key': 'ext.ai.sap.com/customkey2', 'value': 'customvalue2'}] + } + ], + "deployable": False, + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + + def assert_executable(self, e_dict: dict, e: Executable): + e_dict['labels'] = [Label.from_dict(ld) for ld in e_dict['labels']] + e_dict['parameters'] = [Parameter.from_dict(pd) for pd in e_dict['parameters']] + e_dict['input_artifacts'] = [InputArtifact.from_dict(iad) for iad in e_dict['input_artifacts']] + e_dict['output_artifacts'] = [OutputArtifact.from_dict(oad) for oad in e_dict['output_artifacts']] + e_dict['created_at'] = parse_datetime(e_dict['created_at']) + e_dict['modified_at'] = parse_datetime(e_dict['modified_at']) + self.assert_object(e_dict, e) + + def test_get_executable(self): + exc_dict = self.create_executable_dict() + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = copy.deepcopy(exc_dict) + ec = ExecutableClient(rest_client_mock) + exc = ec.get(scenario_id=exc_dict['scenario_id'], executable_id=exc_dict['id'], + resource_group=self.resource_group) + rest_client_mock.get.assert_called_with( + path=f'/scenarios/{exc_dict["scenario_id"]}/executables/{exc_dict["id"]}', + resource_group=self.resource_group) + self.assert_executable(exc_dict, exc) + self.assertIn("Executable id: ", exc.__str__()) + self.assertIn(exc_dict['id'], exc.__str__()) + self.assertIn("Input artifact name: ", exc_dict['input_artifacts'][0].__str__()) + self.assertIn(exc.input_artifacts[0].name, exc_dict['input_artifacts'][0].__str__()) + self.assertIn("Output artifact name: ", exc_dict['output_artifacts'][0].__str__()) + self.assertIn(exc.output_artifacts[0].name, exc_dict['output_artifacts'][0].__str__()) + self.assertIn("Parameter name: ", exc_dict['parameters'][0].__str__()) + self.assertIn(exc.parameters[0].name, exc_dict['parameters'][0].__str__()) + self.assertIn("Parameter type: ", exc_dict['parameters'][0].__str__()) + self.assertIn(exc.parameters[0].type.value, exc_dict['parameters'][0].__str__()) + + def test_query_executables(self): + n = 3 + exc_dicts = [self.create_executable_dict() for _ in range(n)] + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = {'resources': copy.deepcopy(exc_dicts), 'count': n} + ec = ExecutableClient(rest_client_mock) + eqr = ec.query(scenario_id=exc_dicts[0]['scenario_id']) + rest_client_mock.get.assert_called_with(path=f'/scenarios/{exc_dicts[0]["scenario_id"]}/executables', + params=None, resource_group=None) + self.assert_object_lists(exc_dicts, eqr.resources, self.assert_executable) + + params = {'version_id': 'test_version_id'} + sid = 'test_scenario_id' + rest_client_mock.get.return_value = {'resources': [], 'count': 0} + ec.query(scenario_id=sid, resource_group=self.resource_group, **params) + rest_client_mock.get.assert_called_with(path=f'/scenarios/{sid}/executables', params=params, + resource_group=self.resource_group) diff --git a/packages/base/tests/resource_clients/test_execution_client.py b/packages/base/tests/resource_clients/test_execution_client.py new file mode 100644 index 0000000..2262a45 --- /dev/null +++ b/packages/base/tests/resource_clients/test_execution_client.py @@ -0,0 +1,212 @@ +import copy +import uuid +from datetime import datetime + +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT, parse_datetime +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.base_models import Order, BasicModifyRequest +from ai_api_client_sdk.models.execution import Execution +from ai_api_client_sdk.models.execution_bulk_modify_response import ExecutionBulkModifyResponse +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from ai_api_client_sdk.resource_clients.execution_client import ExecutionClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestExecutionClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ExecutionClient(self.rest_client_mock) + + @staticmethod + def create_execution_dict(): + return { + "id": str(uuid.uuid4()), + "configuration_id": str(uuid.uuid4()), + "configuration_name": "test_configuration_name", + "scenario_id": str(uuid.uuid4()), + "target_status": "STOPPED", + "status": "COMPLETED", + "output_artifacts": [ + { + "labels": [ + { + "key": "ext.ai.sap.com/s4hana-version", + "value": "label_value" + } + ], + "name": "test_artifact_name", + "kind": "model", + "url": "https://example.com/some_path", + "description": "test_artifact_description", + "id": str(uuid.uuid4()), + "scenario_id": str(uuid.uuid4()), + "execution_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + ], + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z", + "submission_time": "2021-03-29T12:58:05Z", + "start_time": "2021-03-29T12:58:05Z", + "completion_time": "2021-03-29T12:58:05Z", + "status_message": "Execution completed", + "status_details": {} + } + + def assert_execution(self, e_dict: dict, e: Execution): + e_dict['output_artifacts'] = [Artifact.from_dict(oad) for oad in e_dict['output_artifacts']] + self.assert_enactment(e_dict, e) + + def test_get_execution(self): + exc_dict = self.create_execution_dict() + self.rest_client_mock.get.return_value = copy.deepcopy(exc_dict) + exc = self.client.get(execution_id=exc_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/executions/{exc_dict["id"]}', + resource_group=self.resource_group) + self.assert_execution(exc_dict, exc) + self.assertIn("Execution id: ", exc.__str__()) + self.assertIn(exc_dict['id'], exc.__str__()) + self.assertEqual(exc.status_message, exc_dict['status_message']) + + def test_execution_get_status(self): + response_dict = { + "configuration_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "id": str(uuid.uuid4()), + "modified_at": "2021-03-29T12:58:05Z", + "status": "RUNNING", + "status_details": {} + } + params = {'$select': 'status'} + self.rest_client_mock.get.return_value = response_dict.copy() + exc = self.client.get(execution_id=response_dict['id'], resource_group=self.resource_group, select='status') + self.rest_client_mock.get.assert_called_with(path=f'/executions/{response_dict["id"]}', + params=params, + resource_group=self.resource_group) + self.assertEqual(exc.status, Status(response_dict['status'])) + self.assertEqual(exc.status_details, response_dict['status_details']) + + def test_query_executions(self): + n = 3 + exc_dicts = [self.create_execution_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': copy.deepcopy(exc_dicts), 'count': n} + eqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/executions', params=None, resource_group=None) + self.assert_object_lists(exc_dicts, eqr.resources, self.assert_execution) + + params = {'scenario_id': 'test_scenario_id', 'configuration_id': 'test_configuration_id', + 'executable_ids': ['test_executable_id'], 'execution_schedule_id': 'test_schedule_id', + 'status': Status.COMPLETED, 'top': 5, 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['executable_ids'] = ','.join(params['executable_ids']) + params['status'] = params['status'].value + self.rest_client_mock.get.assert_called_with(path='/executions', params=params, + resource_group=self.resource_group) + + def test_create_execution(self): + exc_dict = self.create_execution_dict() + response_message = 'Execution created' + self.rest_client_mock.post.return_value = {'id': exc_dict['id'], 'message': response_message, + 'status': exc_dict['status']} + ecr = self.client.create(configuration_id=exc_dict['configuration_id'], + resource_group=self.resource_group) + body = {"configuration_id": exc_dict['configuration_id']} + + self.rest_client_mock.post.assert_called_with(path='/executions', body=body, + resource_group=self.resource_group) + self.assertEqual(exc_dict['id'], ecr.id) + self.assertEqual(response_message, ecr.message) + self.assertEqual(Status(exc_dict['status']), ecr.status) + + def test_modify_execution(self): + response_dict = {'id': 'test_execution_id', 'message': 'Execution patched'} + body = {'target_status': TargetStatus.STOPPED} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(execution_id=response_dict['id'], resource_group=self.resource_group, **body) + body['target_status'] = body['target_status'].value + self.rest_client_mock.patch.assert_called_with(path=f'/executions/{response_dict["id"]}', body=body, + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_bulk_modify_executions(self): + executions = [ + BasicModifyRequest("executionid1", TargetStatus.STOPPED), + BasicModifyRequest("executionid2", TargetStatus.DELETED) + ] + body = {'executions': [bmr.to_dict() for bmr in executions]} + response_dict = { + "executions": [ + { + "id": "executionid1", + "message": "Execution modification scheduled" + }, + { + "id": "executionid2", + "error": { + "code": "01010076", + "message": "Current status RUNNING cannot be changed", + "request_id": "string", + "target": "string", + "details": {} + } + } + ] + } + self.rest_client_mock.patch.return_value = copy.deepcopy(response_dict) + ebmr = self.client.bulk_modify(executions=executions, resource_group=self.resource_group) + headers = {'Content-Type': 'application/merge-patch+json'} + self.rest_client_mock.patch.assert_called_with(path=f'/executions', body=body, headers=headers, + resource_group=self.resource_group) + + self.assertIsInstance(ebmr, ExecutionBulkModifyResponse) + self.assertEqual(response_dict['executions'][0]['id'], ebmr.executions[0].id) + self.assertEqual(response_dict['executions'][0]['message'], ebmr.executions[0].message) + self.assertEqual(response_dict['executions'][1]['id'], ebmr.executions[1].id) + self.assertEqual(response_dict['executions'][1]['error']['code'], ebmr.executions[1].error.code) + ebmr_str = ebmr.__str__() + self.assertIn(response_dict['executions'][0]['id'], ebmr_str) + self.assertIn(response_dict['executions'][1]['id'], ebmr_str) + + def test_delete_execution(self): + response_dict = {'id': 'test_execution_id', 'message': 'Execution deleted'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(execution_id=response_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.delete.assert_called_with(path=f'/executions/{response_dict["id"]}', + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_count_executions(self): + self.assert_count('/executions/$count', 7) + self.assert_count('/executions/$count', 6, {'scenario_id': 'test_scenario_id'}) + self.assert_count('/executions/$count', 5, {'configuration_id': 'test_configuration_id'}) + self.assert_count('/executions/$count', 6, {'executable_ids': ['test_executable_id']}) + self.assert_count('/executions/$count', 4, {'execution_schedule_id': 'test_schedule_id'}) + self.assert_count('/executions/$count', 3, {'status': Status.COMPLETED}) + self.assert_count('/executions/$count', 0, + {'scenario_id': 'test_scenario_id', 'configuration_id': 'test_configuration_id', + 'executable_ids': ['test_executable_id'], 'execution_schedule_id': 'test_schedule_id', + 'status': Status.COMPLETED}) + + def test_logs(self): + execution_id = 'test_execution_id' + top = 5 + start = datetime.utcnow() + end = datetime.utcnow() + order = Order.DESC + params = {'top': top, 'start': start.strftime(DATETIME_FORMAT), 'end': end.strftime(DATETIME_FORMAT), + 'order': order.value} + response_dict = {'data': {'result': [{'msg': 'log message', + 'timestamp': '2021-08-27T15:10:36.774534098+00:00'}]}} + self.rest_client_mock.get.return_value = copy.deepcopy(response_dict) + lr = self.client.query_logs(execution_id=execution_id, top=top, start=start, end=end, order=order, + resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/executions/{execution_id}/logs', params=params, + resource_group=self.resource_group) + self.assertEqual(len(response_dict['data']['result']), len(lr.data.result)) + self.assertEqual(response_dict['data']['result'][0]['msg'], lr.data.result[0].msg) + self.assertEqual(parse_datetime(response_dict['data']['result'][0]['timestamp']), lr.data.result[0].timestamp) diff --git a/packages/base/tests/resource_clients/test_execution_schedule_client.py b/packages/base/tests/resource_clients/test_execution_schedule_client.py new file mode 100644 index 0000000..eff6c7c --- /dev/null +++ b/packages/base/tests/resource_clients/test_execution_schedule_client.py @@ -0,0 +1,115 @@ +import copy +import uuid + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime, DATETIME_FORMAT +from ai_api_client_sdk.models.execution_schedule import ExecutionSchedule +from ai_api_client_sdk.models.status import ScheduleStatus +from ai_api_client_sdk.resource_clients.execution_schedule_client import ExecutionScheduleClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestExecutionScheduleClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ExecutionScheduleClient(self.rest_client_mock) + + @staticmethod + def create_execution_schedule_dict(): + return { + "id": str(uuid.uuid4()), + "cron": "0 0 0 1 *", + "name": "test schedule", + "configuration_id": str(uuid.uuid4()), + "status": ScheduleStatus.ACTIVE.value, + "start": "2023-04-05T22:17:25Z", + "end": "2023-04-05T22:17:25Z", + "created_at": "2023-03-29T12:58:05Z", + "modified_at": "2023-03-30T12:58:05Z" + } + + def assert_execution_schedule(self, es_dict: dict, es: ExecutionSchedule): + es_dict['status'] = ScheduleStatus(es_dict['status']) + es_dict['created_at'] = parse_datetime(es_dict['created_at']) + es_dict['modified_at'] = parse_datetime(es_dict['modified_at']) + es_dict['start'] = parse_datetime(es_dict['start']) + es_dict['end'] = parse_datetime(es_dict['end']) + self.assert_object(es_dict, es) + + def test_get_execution_schedule(self): + es_dict = self.create_execution_schedule_dict() + self.rest_client_mock.get.return_value = copy.deepcopy(es_dict) + exec_schedule = self.client.get(execution_schedule_id=es_dict['id'], + resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'/executionSchedules/{es_dict["id"]}', + resource_group=self.resource_group) + self.assert_execution_schedule(es_dict, exec_schedule) + + def test_create_execution_schedule(self): + es_dict = self.create_execution_schedule_dict() + response_message = 'Execution Schedule created' + self.rest_client_mock.post.return_value = {'id': es_dict['id'], 'message': response_message} + ecr = self.client.create(name='test schedule', cron="0 0 0 1 *", configuration_id=es_dict['configuration_id'], + start=parse_datetime(es_dict['start']), resource_group=self.resource_group) + body = { + "name": es_dict['name'], + "cron": es_dict['cron'], + "configuration_id": es_dict['configuration_id'], + "start": es_dict['start'] + } + + self.rest_client_mock.post.assert_called_with(path='/executionSchedules', body=body, + resource_group=self.resource_group) + self.assertEqual(es_dict['id'], ecr.id) + self.assertEqual(response_message, ecr.message) + + def test_query_execution_schedules(self): + n = 3 + es_dicts = [self.create_execution_schedule_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': copy.deepcopy(es_dicts), 'count': n} + eqr = self.client.query() + self.rest_client_mock.get.assert_called_with(path='/executionSchedules', params=None, resource_group=None) + self.assert_object_lists(es_dicts, eqr.resources, self.assert_execution_schedule) + + params = {'configuration_id': 'test_configuration_id', 'status': ScheduleStatus.ACTIVE, 'top': 5, + 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(resource_group=self.resource_group, **params) + params['status'] = params['status'].value + self.rest_client_mock.get.assert_called_with(path='/executionSchedules', params=params, + resource_group=self.resource_group) + + def test_modify_execution_schedule(self): + response_dict = {'id': 'test_execution_schedule_id', 'message': 'Execution Schedule modified'} + body = { + "cron": "1 1 1 1 *", + "start": parse_datetime("2023-04-15T09:00:00Z"), + "end": parse_datetime("2023-05-15T09:00:00Z"), + "configurationId": "aa97b177-9383-4934-8543-0f91a7a0283a", + "status": ScheduleStatus.INACTIVE + } + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(execution_schedule_id=response_dict['id'], resource_group=self.resource_group, **body) + body['start'] = body['start'].strftime(DATETIME_FORMAT) + body['end'] = body['end'].strftime(DATETIME_FORMAT) + body['status'] = body['status'].value + self.rest_client_mock.patch.assert_called_with(path=f'/executionSchedules/{response_dict["id"]}', body=body, + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_delete_execution_schedule(self): + response_dict = {'id': 'test_execution_schedule_id', 'message': 'Execution Schedule deleted'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(execution_schedule_id=response_dict['id'], resource_group=self.resource_group) + self.rest_client_mock.delete.assert_called_with(path=f'/executionSchedules/{response_dict["id"]}', + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_count_execution_schedules(self): + self.assert_count('/executionSchedules/$count', 7) + self.assert_count('/executionSchedules/$count', 5, {'configuration_id': 'test_configuration_id'}) + self.assert_count('/executionSchedules/$count', 3, {'status': ScheduleStatus.INACTIVE}) + self.assert_count('/executionSchedules/$count', 0, + {'configuration_id': 'test_configuration_id', + 'status': ScheduleStatus.ACTIVE}) diff --git a/packages/base/tests/resource_clients/test_healthz_client.py b/packages/base/tests/resource_clients/test_healthz_client.py new file mode 100644 index 0000000..3084af9 --- /dev/null +++ b/packages/base/tests/resource_clients/test_healthz_client.py @@ -0,0 +1,26 @@ +from unittest.mock import MagicMock + +from ai_api_client_sdk.models.healthz_status import HealthStatus +from ai_api_client_sdk.resource_clients.healthz_client import HealthzClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestHealthzClient(ResourceClientTestBase): + @staticmethod + def create_healthz_status_dict(): + return { + "status": "READY", + "message": "test_message" + } + + def test_test(self): + hs_dict = self.create_healthz_status_dict() + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = hs_dict.copy() + hc = HealthzClient(rest_client_mock) + hs = hc.get() + rest_client_mock.get.assert_called_with(path='/healthz') + self.assertEqual(hs_dict['message'], hs.message) + self.assertEqual(HealthStatus(hs_dict['status']), hs.status) + self.assertIn("Healthz status message: ", hs.__str__()) + self.assertIn(hs_dict['message'], hs.__str__()) diff --git a/packages/base/tests/resource_clients/test_meta_client.py b/packages/base/tests/resource_clients/test_meta_client.py new file mode 100644 index 0000000..f5da730 --- /dev/null +++ b/packages/base/tests/resource_clients/test_meta_client.py @@ -0,0 +1,123 @@ +import copy +from unittest.mock import MagicMock + +from ai_api_client_sdk.models.ai_api_meta import AIAPIMeta +from ai_api_client_sdk.models.capabilities import Capabilities +from ai_api_client_sdk.models.extensions import Extensions +from ai_api_client_sdk.models.version_list import VersionList +from ai_api_client_sdk.resource_clients.meta_client import MetaClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestMetaClient(ResourceClientTestBase): + @staticmethod + def create_capabilities_dict(): + return { + "runtime_identifier": "aicore", + "runtime_api_version": "1.2.3", + "description": "test_description", + "ai_api": { + "version": "1.2.3", + "capabilities": { + "multitenant": True, + "shareable": True, + "static_deployments": True, + "user_deployments": True, + "user_executions": True, + "time_to_live_deployments": True, + "bulk_updates": { + "deployments": True, + "executions": True + }, + "execution_schedules": True, + "logs": { + "executions": True, + "deployments": True + } + }, + "limits": { + "executions": { + "max_running_count": -1 + }, + "deployments": { + "max_running_count": -1 + } + } + }, + "extensions": { + "analytics": { + "version": "1.2.3" + }, + "resource_groups": { + "version": "1.2.3" + }, + "dataset": { + "version": "1.2.3", + "capabilities": { + "upload": True, + "download": True, + "delete": True + }, + "limits": { + "max_upload_file_size": 104857600, + "max_files_per_dataset": -1 + } + } + } + } + + @staticmethod + def create_versions_dict(): + return { + "versions": [ + { + "version_id": "test_v1", + "url": "https://api.test.com/v1", + "description": "Test API 1" + }, + { + "version_id": "test_v2", + "url": "https://api.test.com/v2", + "description": "Test API 2" + } + ] + } + + def assert_capabilities(self, c_dict: dict, c: Capabilities): + c_dict['ai_api'] = AIAPIMeta.from_dict(c_dict['ai_api']) + if 'extensions' in c_dict: + c_dict['extensions'] = Extensions.from_dict(c_dict['extensions']) + self.assert_object(c_dict, c) + + def assert_version_list(self, vl_dict: dict, vl: VersionList): + if 'versions' in vl_dict: + self.assert_object_lists(vl_dict['versions'], vl.versions, sort_key='version_id') + del vl_dict['versions'] + self.assert_object(vl_dict, vl) + + def test_get_capabilities(self): + c_dict = self.create_capabilities_dict() + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = copy.deepcopy(c_dict) + mc = MetaClient(rest_client_mock) + c = mc.get() + rest_client_mock.get.assert_called_with(path='/meta') + self.assert_capabilities(c_dict, c) + + def test_get_capabilities_bare_minimum(self): + c_dict = {'ai_api': {'version': '1.2.3'}} + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = copy.deepcopy(c_dict) + mc = MetaClient(rest_client_mock) + c = mc.get() + rest_client_mock.get.assert_called_with(path='/meta') + self.assert_capabilities(c_dict, c) + + def test_get_versions(self): + vl_dict = self.create_versions_dict() + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = copy.deepcopy(vl_dict) + mc = MetaClient(rest_client_mock) + vl = mc.get_versions() + rest_client_mock.get.assert_called_with(path='/meta/versions') + self.assert_version_list(vl_dict, vl) diff --git a/packages/base/tests/resource_clients/test_metrics_client.py b/packages/base/tests/resource_clients/test_metrics_client.py new file mode 100644 index 0000000..a62757d --- /dev/null +++ b/packages/base/tests/resource_clients/test_metrics_client.py @@ -0,0 +1,148 @@ +import copy +import uuid +from datetime import datetime +from unittest.mock import MagicMock + +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_label import MetricLabel +from ai_api_client_sdk.models.metric_resource import MetricResource +from ai_api_client_sdk.models.metric_tag import MetricTag +from ai_api_client_sdk.resource_clients.metrics_client import MetricsClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestMetricsClient(ResourceClientTestBase): + def setUp(self): + n = 3 + self.metric_resource_dicts = [self.create_metric_resource_dict() for _ in range(n)] + self.rest_client_mock = MagicMock() + self.rest_client_mock.get.return_value = {'resources': copy.deepcopy(self.metric_resource_dicts), 'count': n} + self.mc = MetricsClient(self.rest_client_mock) + + @staticmethod + def create_metric_resource_dict(): + return { + "execution_id": str(uuid.uuid4()), + "metrics": [ + { + "name": "Error Rate", + "value": 0.98, + "timestamp": "2021-03-29T12:58:05Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Confusion Matrix", + "value": "test_confusion_matrix" + } + ] + } + + def assert_metric_resources(self, mr_dict: dict, mr: MetricResource): + if 'metrics' in mr_dict: + mr_dict['metrics'] = [Metric.from_dict(md) for md in mr_dict['metrics']] + if 'tags' in mr_dict: + mr_dict['tags'] = [MetricTag.from_dict(mtd) for mtd in mr_dict['tags']] + if 'custom_info' in mr_dict: + mr_dict['custom_info'] = [MetricCustomInfo.from_dict(mcid) for mcid in mr_dict['custom_info']] + + def test_query_metrics(self): + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id']} + + mqr = self.mc.query(filter=params['$filter'], execution_ids=params['execution_ids'], + resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + self.assertIn("Metric execution id: ", mqr.resources[0].__str__()) + self.assertIn(self.metric_resource_dicts[0]['execution_id'], mqr.resources[0].__str__()) + self.assertIn("Metrics: ", mqr.resources[0].__str__()) + self.assertIn(self.metric_resource_dicts[0]['metrics'][0].name, mqr.resources[0].__str__()) + self.assertIn("Metric tag name: ", mqr.resources[0].tags[0].__str__()) + self.assertIn(self.metric_resource_dicts[0]['tags'][0].name, mqr.resources[0].tags[0].__str__()) + self.assertIn("Metric tag value: ", mqr.resources[0].tags[0].__str__()) + self.assertIn(self.metric_resource_dicts[0]['tags'][0].value, mqr.resources[0].tags[0].__str__()) + + def test_query_metrics_with_select(self): + select = 'metrics,tags' + select_list = select.split(',') + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id'], '$select': select} + + mqr = self.mc.query(filter=params['$filter'], execution_ids=params['execution_ids'], select=select_list, + resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + response_with_metrics_tags = [] + for execution_data in self.metric_resource_dicts: + new_execution_data = {} + new_execution_data['execution_id'] = execution_data['execution_id'] + for select in select_list: + new_execution_data[select] = execution_data[select] + response_with_metrics_tags.append(new_execution_data) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(response_with_metrics_tags, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_query_with_only_execution_ids(self): + params = {'execution_ids': ['test_exec_id']} + mqr = self.mc.query(execution_ids=params['execution_ids'], resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_query_metrics_with_no_parameters(self): + mqr = self.mc.query(resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=None, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_delete_metrics(self): + execution_id = 'test_exec_id' + self.mc.delete(execution_id=execution_id, resource_group=self.resource_group) + params = {'execution_id': execution_id} + self.rest_client_mock.delete.assert_called_with(path='/metrics', params=params, + resource_group=self.resource_group) + + def test_metric_to_dict(self): + metric = Metric(name='test_metric_name', value=0.0, timestamp=datetime.utcnow(), step=1, + labels=[MetricLabel(name='test_label_name', value='test_label_value')]) + metric_dict = metric.to_dict() + self.assertEqual(metric.name, metric_dict['name']) + self.assertEqual(metric.value, metric_dict['value']) + self.assertEqual(metric.step, metric_dict['step']) + self.assertTrue(isinstance(metric.timestamp, datetime)) + self.assertEqual(metric.timestamp.strftime(DATETIME_FORMAT), metric_dict['timestamp']) + self.assertEqual(metric.labels[0].to_dict(), metric_dict['labels'][0]) + self.assertIn("Metric name", metric.__str__()) + self.assertIn(str(metric.name), metric.__str__()) + self.assertIn("Metric value", metric.__str__()) + self.assertIn(str(metric.value), metric.__str__()) + + def test_metric_default_values(self): + metric = Metric(name='test_name', value=0.0, timestamp=datetime.utcnow()) + self.assertEqual(0, metric.step) + self.assertEqual([], metric.labels) + + test_step = 1 + test_labels = [MetricLabel(name='test_label_name', value='test_label_value')] + metric = Metric(name='test_name', value=0.0, timestamp=datetime.utcnow(), step=test_step, + labels=test_labels) + self.assertEqual(test_step, metric.step) + self.assertEqual(test_labels, metric.labels) + diff --git a/packages/base/tests/resource_clients/test_model_client.py b/packages/base/tests/resource_clients/test_model_client.py new file mode 100644 index 0000000..3399c16 --- /dev/null +++ b/packages/base/tests/resource_clients/test_model_client.py @@ -0,0 +1,88 @@ +import copy +from unittest.mock import MagicMock + +import humps + +from ai_api_client_sdk.models.model import Model +from ai_api_client_sdk.models.model_version import ModelVersion +from ai_api_client_sdk.resource_clients.model_client import ModelClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestModelClient(ResourceClientTestBase): + @staticmethod + def create_model_dict(): + # humps.decamelize usually called by request method + return humps.decamelize( + { + "description": "Mistral mixtral-8x7b-instruct-v01 model", + "executableId": "aicore-opensource", + "model": "mistralai--mixtral-8x7b-instruct-v01", + "versions": [ + { + "isLatest": True, + "name": "1.2", + "deprecated": False, + "retirementDate": "2025-01-01", + "contextLength": 2048, + "inputTypes": ["text", "image"], + "capabilities": ["classification", "generation"], + "metadata": {"key1": "value1"}, + "cost": {"unit": "USD", "value": "0.01"}, + }, + { + "isLatest": False, + "name": "1.1", + "deprecated": True, + "retirementDate": "2025-01-01", + }, + ], + "displayName": "Mistral Model", + "accessType": "public", + "provider": "MistralAI", + "allowedScenarios": [ + {"scenarioId": "scenario1", "executableId": "exec1"}, + ], + } + ) + + def assert_model_version(self, mv_dict: dict, mv: ModelVersion): + self.assert_object(mv_dict, mv) + + def assert_model(self, m_dict: dict, m: Model): + m_dict["versions"] = [ModelVersion.from_dict(mv) for mv in m_dict["versions"]] + self.assert_object(m_dict, m) + + def test_query_models(self): + n = 3 + model_dicts = [self.create_model_dict() for _ in range(n)] + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = { + "resources": copy.deepcopy(model_dicts), + "count": n, + } + ec = ModelClient(rest_client_mock) + eqr = ec.query() + rest_client_mock.get.assert_called_with( + path="/scenarios/foundation-models/models", + resource_group=None, + ) + self.assert_object_lists( + copy.deepcopy(model_dicts), + eqr.resources, + self.assert_model, + sort_key="model", + ) + self.assert_object_lists( + copy.deepcopy(model_dicts[0]["versions"]), + eqr.resources[0].versions, + self.assert_model_version, + sort_key="name", + ) + + rest_client_mock.get.return_value = {"resources": [], "count": 0} + ec.query(resource_group=self.resource_group) + rest_client_mock.get.assert_called_with( + path="/scenarios/foundation-models/models", + resource_group=self.resource_group, + ) diff --git a/packages/base/tests/resource_clients/test_resource_groups_client.py b/packages/base/tests/resource_clients/test_resource_groups_client.py new file mode 100644 index 0000000..14a6324 --- /dev/null +++ b/packages/base/tests/resource_clients/test_resource_groups_client.py @@ -0,0 +1,131 @@ +from ai_api_client_sdk.models.resource_group import ResourceGroup +from ai_api_client_sdk.models.resource_group_query_response import ResourceGroupQueryResponse +from ai_api_client_sdk.resource_clients.resource_groups_client import ResourceGroupsClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestDockerRegistrySecretsClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ResourceGroupsClient(self.rest_client_mock) + self.rg_path = '/admin/resourceGroups' + + def assert_resource_group(self, rg_expected: ResourceGroup, rg: ResourceGroup): + self.assertEqual(rg_expected.resource_group_id, rg.resource_group_id) + self.assertEqual(rg_expected.labels, rg.labels) + self.assertEqual(rg_expected.status, rg.status) + self.assertEqual(rg_expected.created_at, rg.created_at) + + @staticmethod + def create_resource_group_dict(): + return { + 'resource_group_id': 'testrg', + 'labels': [ + { + 'key': 'ext.ai.sap.com/label1', + 'value': 'value1', + }, + { + 'key': 'ext.ai.sap.com/label2', + 'value': 'value2', + }, + ], + 'status': 'test_status', + 'created_at': '2021-03-29T12:58:05Z' + } + + @staticmethod + def create_resource_group(): + return ResourceGroup.from_dict(TestDockerRegistrySecretsClient.create_resource_group_dict()) + + def test_create_resource_group(self): + self.rest_client_mock.post.return_value = self.create_resource_group_dict() + rg = self.create_resource_group() + response = self.client.create(resource_group_id=rg.resource_group_id, labels=rg.labels) + body = { + 'resourceGroupId': rg.resource_group_id, + 'labels': [l.to_dict() for l in rg.labels], + } + self.rest_client_mock.post.assert_called_with(path=self.rg_path, body=body) + self.assert_resource_group(rg, response) + + def test_delete_resource_group(self): + test_rg_name = 'test_resource_group_name' + response_dict = { + 'id': test_rg_name, + 'message': 'Resource Group deleted', + } + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(resource_group_id=test_rg_name) + self.rest_client_mock.delete.assert_called_with(path=f'{self.rg_path}/{test_rg_name}') + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_get_resource_group(self): + rg_dict = self.create_resource_group_dict() + resource_group_id = rg_dict['resource_group_id'] + self.rest_client_mock.get.return_value = rg_dict.copy() + rg = self.client.get(resource_group_id=resource_group_id) + self.rest_client_mock.get.assert_called_with(path=f'{self.rg_path}/{resource_group_id}') + + expected_rg = self.create_resource_group() + self.assert_resource_group(expected_rg, rg) + self.assertIn("Resource group id: ", rg.__str__()) + self.assertIn(rg_dict['resource_group_id'], rg.__str__()) + + def test_get_resource_group_aicore(self): + rg_dict = self.create_resource_group_dict() + resource_group_id = rg_dict['resource_group_id'] + mock_rg = rg_dict.copy() + mock_rg['zoneId'] = 'test-zone-id' + self.rest_client_mock.get.return_value = mock_rg + rg = self.client.get(resource_group_id=resource_group_id) + self.rest_client_mock.get.assert_called_with(path=f'{self.rg_path}/{resource_group_id}') + + expected_rg = self.create_resource_group() + self.assert_resource_group(expected_rg, rg) + + def test_modify_resource_group(self): + rg = self.create_resource_group() + self.rest_client_mock.modify.return_value = "" + self.client.modify(resource_group_id=rg.resource_group_id, labels=rg.labels) + body = { + 'labels': [l.to_dict() for l in rg.labels], + } + self.rest_client_mock.patch.assert_called_with(path=f'{self.rg_path}/{rg.resource_group_id}', body=body) + + def test_query_resource_group(self): + self.rest_client_mock.get.return_value = { + 'resources': [ + self.create_resource_group_dict(), + ], + 'count': 1, + } + response = self.client.query() + self.rest_client_mock.get.assert_called_with(path=f'{self.rg_path}', params=None) + + expected_response = ResourceGroupQueryResponse([self.create_resource_group()], 1) + self.assertEqual(expected_response.count, response.count) + self.assertEqual(len(expected_response.resources), len(response.resources)) + + self.assert_resource_group(expected_response.resources[0], response.resources[0]) + + def test_query_resource_group_search_case_insensitive(self): + self.rest_client_mock.get.return_value = { + 'resources': [ + self.create_resource_group_dict(), + ], + 'count': 1, + } + params = {'search': 'test_search', 'search_case_insensitive': True} + response = self.client.query(**params) + + params['$search'] = params['search'] + del params['search'] + self.rest_client_mock.get.assert_called_with(path=f'{self.rg_path}', params=params) + + expected_response = ResourceGroupQueryResponse([self.create_resource_group()], 1) + self.assertEqual(expected_response.count, response.count) + self.assertEqual(len(expected_response.resources), len(response.resources)) + + self.assert_resource_group(expected_response.resources[0], response.resources[0]) \ No newline at end of file diff --git a/packages/base/tests/resource_clients/test_scenario_client.py b/packages/base/tests/resource_clients/test_scenario_client.py new file mode 100644 index 0000000..8fb9075 --- /dev/null +++ b/packages/base/tests/resource_clients/test_scenario_client.py @@ -0,0 +1,131 @@ +import copy +import uuid +from unittest.mock import MagicMock + +from ai_api_client_sdk.helpers.datetime_parser import parse_datetime +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.scenario import Scenario +from ai_api_client_sdk.models.version import Version +from ai_api_client_sdk.resource_clients.scenario_client import ScenarioClient +from .resource_client_test_base import ResourceClientTestBase + + +class TestScenarioClient(ResourceClientTestBase): + @staticmethod + def create_scenario_dict(): + return { + "labels": [ + { + "key": "ext.ai.sap.com/s4hana-version", + "value": "test_label_value" + } + ], + "name": "test_scenario_name", + "description": "test_scenario_description", + "id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + + @staticmethod + def create_llm_scenario_dict(): + return { + "labels": [ + { + "key": "scenarios.ai.sap.com/llm", + "value": "true" + } + ], + "name": "test_scenario_name", + "description": "test_scenario_description", + "id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + + @staticmethod + def create_version_dict(): + return { + "description": "This is version v1", + "id": "test_version_id", + "scenario_id": str(uuid.uuid4()), + "created_at": "2021-03-29T12:58:05Z", + "modified_at": "2021-03-29T12:58:05Z" + } + + def assert_scenario(self, s_dict: dict, s: Scenario): + s_dict['labels'] = [Label.from_dict(ld) for ld in s_dict['labels']] + s_dict['created_at'] = parse_datetime(s_dict['created_at']) + s_dict['modified_at'] = parse_datetime(s_dict['modified_at']) + self.assert_object(s_dict, s) + + def assert_version(self, v_dict: dict, v: Version): + v_dict['created_at'] = parse_datetime(v_dict['created_at']) + v_dict['modified_at'] = parse_datetime(v_dict['modified_at']) + self.assert_object(v_dict, v) + + def test_get_scenario(self): + s_dict = self.create_scenario_dict() + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = copy.deepcopy(s_dict) + sc = ScenarioClient(rest_client_mock) + s = sc.get(scenario_id=s_dict['id'], resource_group=self.resource_group) + rest_client_mock.get.assert_called_with(path=f'/scenarios/{s_dict["id"]}', resource_group=self.resource_group) + self.assert_scenario(s_dict, s) + self.assertIn("Scenario id: ", s.__str__()) + self.assertIn(s_dict['id'], s.__str__()) + + def test_query_scenarios(self): + n = 3 + s_dicts = [self.create_scenario_dict() for _ in range(n)] + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = {'resources': copy.deepcopy(s_dicts), 'count': n} + sc = ScenarioClient(rest_client_mock) + sqr = sc.query() + rest_client_mock.get.assert_called_with(path='/scenarios', resource_group=None) + self.assert_object_lists(s_dicts, sqr.resources, self.assert_scenario) + + def test_query_scenarios_for_llm(self): + n = 3 + s_dicts = [self.create_scenario_dict() for _ in range(n)] + s_llm = self.create_llm_scenario_dict() + s_dicts.append(s_llm) + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = {'resources': copy.deepcopy(s_dicts), 'count': n+1} + sc = ScenarioClient(rest_client_mock) + scenario_llm_query = sc.query(only_llm_scenarios=True) + rest_client_mock.get.assert_called_with(path='/scenarios', resource_group=None) + self.assert_object_lists([s_llm], scenario_llm_query.resources, self.assert_scenario) + + def test_query_llm_scenarios(self): + n = 3 + s_dicts = [self.create_scenario_dict() for _ in range(n)] + s_llm = self.create_llm_scenario_dict() + s_dicts.append(s_llm) + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = {'resources': copy.deepcopy(s_dicts), 'count': n + 1} + sc = ScenarioClient(rest_client_mock) + scenario_llm_query = sc.query_llm_scenarios() + rest_client_mock.get.assert_called_with(path='/scenarios', resource_group=None) + self.assert_object_lists([s_llm], scenario_llm_query.resources, self.assert_scenario) + + def test_query_versions(self): + n = 3 + ver_dicts = [self.create_version_dict() for _ in range(n)] + rest_client_mock = MagicMock() + rest_client_mock.get.return_value = {'resources': copy.deepcopy(ver_dicts), 'count': n} + sc = ScenarioClient(rest_client_mock) + vqr = sc.query_versions(scenario_id=ver_dicts[0]['scenario_id']) + rest_client_mock.get.assert_called_with(path=f'/scenarios/{ver_dicts[0]["scenario_id"]}/versions', + params=None, resource_group=None) + self.assert_object_lists(ver_dicts, vqr.resources, self.assert_version) + + params = {'label_selector': ['test_label_selector']} + sid = 'test_scenario_id' + rest_client_mock.get.return_value = {'resources': [], 'count': 0} + sc.query_versions(scenario_id=sid, resource_group=self.resource_group, **params) + params['label_selector'] = ','.join(params['label_selector']) + rest_client_mock.get.assert_called_with(path=f'/scenarios/{sid}/versions', params=params, + resource_group=self.resource_group) + self.assertIn("Version id: ", vqr.resources[0].__str__()) + self.assertIn(ver_dicts[0]['id'], vqr.resources[0].__str__()) diff --git a/packages/base/tests/test_ai_api_v2_client.py b/packages/base/tests/test_ai_api_v2_client.py new file mode 100644 index 0000000..4465964 --- /dev/null +++ b/packages/base/tests/test_ai_api_v2_client.py @@ -0,0 +1,107 @@ +import os +from unittest import TestCase + +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client, AUTH_PARAM_ERROR_MESSAGE +from ai_api_client_sdk.helpers.constants import SKIP_AUTH_ENV_VAR +from ai_api_client_sdk.exception import AIAPIAuthenticatorException + + +class TestAIAPIV2Client(TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.base_url = 'test_base_url' + cls.auth_url = 'test_auth_url' + cls.client_id = 'test_client_id' + cls.client_secret = 'test_client_secret' + cls.cert_str = 'test_cert_str' + cls.key_str = 'test_key_str' + cls.cert_file_path = 'cert_file_path' + cls.key_file_path = 'key_file_path' + + @staticmethod + def token_generator(): + return 'test_token' + + def test_no_secret_no_cert_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + c = AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url) + self.assertTrue('client_id' in cm.exception.error_message) + + def test_happy_path_client_secret(self): + c = AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + client_secret=self.client_secret) + self.assertIsNotNone(c.rest_client) + + def test_happy_path_token_creator(self): + c = AIAPIV2Client(base_url=self.base_url, token_creator=self.token_generator) + self.assertIsNotNone(c.rest_client) + + def test_set_client_type_header(self): + c = AIAPIV2Client(base_url=self.base_url, token_creator=self.token_generator, client_type='test_client_type') + self.assertIsNotNone(c.rest_client) + self.assertEqual(c.rest_client.headers['AI-Client-Type'], 'test_client_type') + + def test_happy_path_with_x509_str(self): + c = AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_str=self.cert_str, key_str=self.key_str) + self.assertIsNotNone(c.rest_client) + + def test_happy_path_with_x509_file_path(self): + c = AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + self.assertIsNotNone(c.rest_client) + + def test_token_creator_with_auth_parameters_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, token_creator=self.token_generator, auth_url=self.auth_url) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, token_creator=self.token_generator, cert_str=self.cert_str) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, token_creator=self.token_generator, key_file_path=self.key_file_path) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_client_secret_with_x509_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + client_secret=self.client_secret, cert_file_path=self.cert_file_path) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + client_secret=self.client_secret, key_str=self.key_str) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_x509_str_with_file_path_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path, cert_str=self.cert_str) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + key_file_path=self.key_file_path, cert_str=self.cert_str, key_str=self.key_str) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + with self.assertRaises(AIAPIAuthenticatorException) as cm: + AIAPIV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path, cert_str=self.cert_str, + key_str=self.key_str) + self.assertEqual(AUTH_PARAM_ERROR_MESSAGE, cm.exception.error_message) + + def test_skip_authorization(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + c = AIAPIV2Client(base_url=self.base_url) + self.assertTrue('client_id' in cm.exception.error_message) + + # Set SKIP_AUTHORIZATION to 'true' + os.environ[SKIP_AUTH_ENV_VAR] = 'true' + c = AIAPIV2Client(base_url=self.base_url) + self.assertIsNotNone(c.rest_client) + + # Clean up + del os.environ[SKIP_AUTH_ENV_VAR] diff --git a/packages/core/PYPIDESCRIPTION.md b/packages/core/PYPIDESCRIPTION.md new file mode 100644 index 0000000..d337b1a --- /dev/null +++ b/packages/core/PYPIDESCRIPTION.md @@ -0,0 +1,221 @@ +# SAP Cloud SDK for AI (Python): Core SDK +The SDK formerly known as *AI Core SDK* was rebranded. + +The class names have not changed i.e., you can continue to use existing code. + +The SAP AI Core SDK can be used to interact with SAP AI Core. +It provides access to all public lifecycle and administration APIs. + +For example: + +* You can execute pipelines as a batch job to preprocess or train your models, or perform batch inference. + +* You can deploy а trained machine learning model as a web service to serve inference requests with high performance. + +* You can register your own Docker registry, synchronize your AI content from your own git repository, and register your own object store for training data and trained models. + +* You can log metrics within a workflow execution using the SDK. You can use the same code for tracking metrics in both your local environment and in the workflow execution (production). + +> **Notes** +> +> - Executing online inference is not part of Core SDK. +> +> - Metrics persistence is not currently available in your local environment using the SDK. However, it is available in your productive workflow execution. +> +> - *Content packages* for AICore are no longer supported. +> +## Example Usage + +Here are a few examples how to use this SDK. +For details on the methods, please refer to the [API documentation](https://api.sap.com/api/AI_CORE_API/resource/Scenario). + +### Import Definitions + +```python +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +``` + +## Create Client + +The SDK requires credentials from your tenant's subaccount Service Key: +```python +client = AICoreV2Client(base_url=AI_API_BASE, + auth_url=AUTH_URL, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + resource_group=resource_group_id) +``` +(For persistent client configuration see below.) + +### Create New Resource Group + +```python +resource_group_create = client.resource_groups.create(resource_group_id=resource_group_id) +print(resource_group_create.resource_group_id) +resource_group_details = client.resource_groups.get(resource_group_id=resource_group_id) +print(f"{resource_group_details.status_message} \n{resource_group_details.resource_group_id}") +``` + +### Create Object Store Secret + +```python +# access key and secret are assumed to reside in environment variables OSS_KEY and OSS_SECRET +object_store_secret_create = client.object_store_secrets.create( + name="default", + type="S3", + bucket="", + endpoint="", + path_prefix="", region="", + data={"AWS_ACCESS_KEY_ID": os.environ.get("OSS_KEY"), + "AWS_SECRET_ACCESS_KEY": os.environ.get("OSS_SECRET")}) + +secret_get = client.object_store_secrets.get(name="default") +print(f"{secret_get.metadata}") +``` + +### List Scenarios + +```python +scenarios = client.scenario.query() +for scenario in scenarios.resources: + print(f"{scenario.name} {scenario.id}") +``` +## Client Configuration + +There are different options to persist the client credentials +(in this order of precedence): + - in code via keyword arguments (see above), + - environment variables, + - profile configuration file. + - from VCAP_SERVICES environment variable, if exists + +A **profile** is a json file residing in a config directory, +which can be set via environment variable `AICORE_HOME` (the default being `~/.aicore/config.json`). + +The command `aicore configure --help` shows the options for generating a profile. + +With profile names one can switch easily between profiles e.g., for different (sub)accounts. +The profile name can be passed also as a keyword. If no profile is specified, the default profile is used. + +## Tracking + + The tracking module of the SAP AI Core SDK can be used to log metrics in both your local environment, and productive workflow executions. Metrics persistence is currently available in your productive environment. + + Here are a few code samples demonstrating how to use the SDK for metrics tracking. + + +### Modify Metrics + + ``` + from ai_core_sdk.tracking import Tracking + + from ai_core_sdk.models import Metric, MetricTag, MetricCustomInfo + + tracking_client = Tracking() + + tracking_client.modify( + tags = [ + # list + MetricTag(name="Our Team Tag", value="Tutorial Team"), + MetricTag(name="Stage", value="Development") + ], + metrics = [ + Metric( + name="Training Loss", + value=np.finfo(np.float64).max, + timestamp= datetime.now().utcnow(), + step = 1, # denotes epoch 1 + labels = [] + ) + ], + custom_info = [ + # list of Custom Information + MetricCustomInfo( + name = "My Classification Report", + # you may convert anything to string and store it + value = str('''{ + "Cats": { + "Precision": 75, + "Recall": 74 + }, + "Dogs": { + "Precision": 85, + "Recall": 84 + } + }''') + ) + ] + ) + + ``` + + ### Log Metrics + + ``` + tracking_client.log_metrics( + metrics = [ + Metric( + name="Training Loss", + value=float(86.99), + timestamp= datetime.now().utcnow(), + step = 1, # denotes epoch 1 + labels = [] + ), + ], + ) + + ``` + + ### Set Tags + + ``` + tracking_client.set_tags( + tags = [ + # list + MetricTag(name="Our Team Tag", value="Tutorial Team"), + MetricTag(name="Stage", value="Development") + ] + ) + + ``` + + ### Set Custom Info + + ``` + tracking_client.set_custom_info( + custom_info = [ + # list of Custom Information + MetricCustomInfo( + name = "My Classification Report", + # you may convert anything to string and store it + value = str(''' + { + "Cats": { + "Precision": 75, + "Recall": 74 + }, + "Dogs": { + "Precision": 85, + "Recall": 84 + } + } + ''' + ) + ), + ] + ) + + ``` + ### Query Metrics + + ``` + metrics_response = tracking_client.query(execution_ids = [ + "test_execution_id" # Change this with the training execution id + ]) + ``` + + ### Delete Metrics + + ``` + metrics_response = tracking_client.delete(execution_id = "test_execution_id") # Change this with the actual execution id + ``` diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..ec50ff4 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,19 @@ +# SAP Cloud SDK for AI (Python): Core SDK +The Core SDK can be used to interact with SAP AI Core's lifecycle and administration APIs. +It extends the functionality of the sap-ai-sdk-base library. + +The SDK formerly known as *AI Core SDK* was rebranded. + +Use the new package name to install the SDK: +``` +pip install sap-ai-sdk-core +``` +The class names have not changed i.e., you can continue to use existing code. + +**Note**: Content packages for AICore are no longer supported. + +Renovate is set up for this repository. For further information, take a look at the [documentation in ml-api-facade](https://github.wdf.sap.corp/AI/ml-api-facade/blob/master/docs/renovate.md). + +## Releasing to public PyPI + +Please follow [this guide](https://wiki.wdf.sap.corp/wiki/display/AI/Delivering+artifacts+to+PyPI) to release sap-ai-sdk-core to public PyPI repositories. diff --git a/packages/core/ai_core_sdk/__init__.py b/packages/core/ai_core_sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/ai_core_sdk/ai_core_v2_client.py b/packages/core/ai_core_sdk/ai_core_v2_client.py new file mode 100644 index 0000000..4910d9d --- /dev/null +++ b/packages/core/ai_core_sdk/ai_core_v2_client.py @@ -0,0 +1,156 @@ +from ai_core_sdk.helpers.logging import get_logger +from typing import Callable +import os + +from ai_core_sdk.helpers import is_within_aicore +from ai_core_sdk.resource_clients import ( + AIAPIV2Client, + ArtifactClient, + ConfigurationClient, + DeploymentClient, + ExecutableClient, + ExecutionClient, + RestClient, + ScenarioClient, + ResourceGroupsClient, + MetaClient, + ModelClient, +) +from ai_core_sdk.resource_clients.applications_client import ApplicationsClient +from ai_core_sdk.resource_clients.docker_registry_secrets_client import DockerRegistrySecretsClient +from ai_core_sdk.resource_clients.internal_rest_client import InternalRestClient +from ai_core_sdk.resource_clients.metrics_client import MetricsCoreClient +from ai_core_sdk.resource_clients.object_store_secrets_client import ObjectStoreSecretsClient +from ai_core_sdk.resource_clients.kpi_client import KpiClient +from ai_core_sdk.resource_clients.repositories_client import RepositoriesClient +from ai_core_sdk.resource_clients.secrets_client import SecretsClient +from ai_core_sdk.helpers.constants import Timeouts +from ai_core_sdk.credentials import fetch_credentials + + +class AICoreV2Client: + """The AICoreV2Client is the class implemented to interact with the AI Core endpoints. The user can use its + attributes corresponding to the resources, for interacting with endpoints related to that resource. (i.e., + aicoreclient.scenario) + + :param base_url: Base URL of the AI Core. Should include the base path as well. (i.e., "/lm/scenarios" + should work) + :type base_url: str + :param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to + None + :type auth_url: str, optional + :param client_id: client id to be used for authorization, defaults to None + :type client_id: str, optional + :param client_secret: client secret to be used for authorization, defaults to None + :type client_secret: str, optional + :param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None + :type cert_str: str, optional + :param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None + :type key_str: str, optional + :param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter, + defaults to None + :type cert_file_path: str, optional + :param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter, + defaults to None + :type key_file_path: str, optional + :param token_creator: the function which returns the Bearer token, when called. Either this, or + auth_url & client_id & client_secret should be specified, defaults to None + :type token_creator: Callable[[], str], optional + :param resource_group: The default resource group which will be used while sending the requests to the server. If + not set, the resource_group should be specified with every request to the server, defaults to None + :type resource_group: str, optional + :param client_type: The client type which will be sent in the User-Agent header of the requests, defaults to + "AI Core Python SDK" + :type client_type: str, optional + :param \\**kwargs: + * *read_timeout* (int): Read timeout for requests in seconds, defaults to 60s + * *connect_timeout* (int): Connect timeout for requests in seconds, defaults to 60s + * *num_request_retries* (int): Number of retries for failing requests with http status code 429, 500, 502, 503 or 504, defaults to 10 + """ + logger = get_logger() + + # pylint: disable=too-many-arguments + def __init__(self, base_url: str, auth_url: str = None, client_id: str = None, client_secret: str = None, + cert_str: str = None, key_str: str = None, cert_file_path: str = None, key_file_path: str = None, + token_creator: Callable[[], str] = None, resource_group: str = None, + client_type: str = "AI Core Python SDK", + **kwargs): + self.base_url: str = base_url + ai_api_base_url = f'{base_url}/lm' + token_creator = AIAPIV2Client._create_token_creator_if_does_not_exist( + token_creator=token_creator, auth_url=auth_url, client_id=client_id, client_secret=client_secret, + cert_str=cert_str, key_str=key_str, cert_file_path=cert_file_path, key_file_path=key_file_path) + + read_timeout = kwargs.get('read_timeout', Timeouts.READ_TIMEOUT.value) + connect_timeout = kwargs.get('connect_timeout', Timeouts.CONNECT_TIMEOUT.value) + num_request_retries = kwargs.get('num_request_retries', Timeouts.NUM_REQUEST_RETRIES.value) + + ai_api_v2_client = AIAPIV2Client(base_url=ai_api_base_url, token_creator=token_creator, + resource_group=resource_group, read_timeout=read_timeout, + connect_timeout=connect_timeout, num_request_retries=num_request_retries, + client_type=client_type) + + self.rest_client: RestClient = RestClient(base_url=base_url, get_token=token_creator, + resource_group=resource_group, read_timeout=read_timeout, + connect_timeout=connect_timeout, + num_request_retries=num_request_retries, + client_type=client_type) + self.artifact: ArtifactClient = ai_api_v2_client.artifact + self.configuration: ConfigurationClient = ai_api_v2_client.configuration + self.deployment: DeploymentClient = ai_api_v2_client.deployment + self.executable: ExecutableClient = ai_api_v2_client.executable + self.execution: ExecutionClient = ai_api_v2_client.execution + self.resource_groups: ResourceGroupsClient = ai_api_v2_client.resource_groups + self.meta: MetaClient = ai_api_v2_client.meta + self.model: ModelClient = ai_api_v2_client.model + # If the environment variables have AICORE_EXECUTION_ID and AICORE_TRACKING_ENDPOINT, + # it indicates the sdk is used within the training pod + # Initiating an internal rest client if within the training pod + # Else initiating the rest client from ai_api_v2_client + if is_within_aicore(): + self.metrics: MetricsCoreClient = MetricsCoreClient( + rest_client=InternalRestClient( + client_type=client_type, + read_timeout=read_timeout, + connect_timeout=connect_timeout, + num_request_retries=num_request_retries, + ), + execution_id=os.getenv("AICORE_EXECUTION_ID"), + ) + else: + self.metrics: MetricsCoreClient = MetricsCoreClient(rest_client=ai_api_v2_client.rest_client) + self.scenario: ScenarioClient = ai_api_v2_client.scenario + self.docker_registry_secrets: DockerRegistrySecretsClient = DockerRegistrySecretsClient( + rest_client=self.rest_client) + self.applications: ApplicationsClient = ApplicationsClient(rest_client=self.rest_client) + self.object_store_secrets: ObjectStoreSecretsClient = ObjectStoreSecretsClient(rest_client=self.rest_client) + self.secrets: SecretsClient = SecretsClient(rest_client=self.rest_client) + self.kpis: KpiClient = KpiClient(rest_client=self.rest_client) + self.repositories: RepositoriesClient = RepositoriesClient(rest_client=self.rest_client) + + @staticmethod + def from_env(profile_name: str = None, + **kwargs): + """Alternative way to create an AICoreV2Client object. + Parameters for base_url, auth_url, client_id, client_secret, x.509 credentials (either as file path or string) + and resource_group can be passed as keyword or are pulled from environment variables. + It is also possible to use a profile, which is a json file in the config directory. The profile name can be + passed as keyword or is pulled from the environment variable AICORE_PROFILE. If no profile is specified, + the default profile is used. + A specific path to a config, that should be used, can be set via the environment variable AICORE_CONFIG. + The hierarchy of precedence is: + 1. keyword argument + 2. environment variable + 3. configuration file + 4. value from VCAP_SERVICES environment variable, if exists + + :param profile_name: name of the profile to use, defaults to None. If None is passed, the profile is read from + the environment variable AICORE_PROFILE. If this is not set, the default profile is used. + The default profile is read from $AICORE_HOME/config.json. + :type profile_name: optional, str + **kwargs: check the parameters of the class constructor + """ + env_credentials = fetch_credentials(profile=profile_name, **kwargs) + + kwargs.update(env_credentials) + return AICoreV2Client(**kwargs) diff --git a/packages/core/ai_core_sdk/cli.py b/packages/core/ai_core_sdk/cli.py new file mode 100644 index 0000000..78033e9 --- /dev/null +++ b/packages/core/ai_core_sdk/cli.py @@ -0,0 +1,172 @@ +from __future__ import annotations +from typing import Optional +import json +import pathlib +from urllib.parse import urlparse + +from ai_core_sdk.credentials import CORE_CREDENTIAL_VALUES, get_nested_value +from ai_core_sdk.helpers import get_home +from ai_core_sdk.helpers.constants import AI_CORE_PREFIX + +import click + +# Constants +MAX_TRIES = 5 +OAUTH_TOKEN_SUFFIX = '/oauth/token' +API_V2_SUFFIX = '/v2' +DEFAULT_CONFIG = 'config.json' +DEFAULT_RESOURCE_GROUP = 'default' +DEFAULT_PROFILE = 'default' + +# Utility Functions +def create_config(**kwargs): + return {f'{AI_CORE_PREFIX}_{k}'.upper(): v for k, v in kwargs.items() if v is not None} + +def is_valid_url(url, path_forbidden=True): + try: + result = urlparse(url) + return all([result.scheme, result.netloc, not result.path if path_forbidden else True]) + except ValueError: + return False + +def prompt_for_input(prompt_text, is_url=False, path_forbidden=True): + url = None + for _ in range(MAX_TRIES): + user_input = click.prompt(prompt_text, type=str).rstrip('/') + if is_url and not is_valid_url(user_input, path_forbidden): + click.echo('Input is not a valid URL.') + if path_forbidden: + click.echo('Enter URL without any additional path or trailing slash.') + else: + url = user_input + break + + if url is None: + raise ValueError('Max tries reached!') + return url + +# CLI Functions +@click.group() +@click.option('-p', '--profile', default=DEFAULT_PROFILE, type=str) +@click.pass_context +def cli(ctx, profile): + """CLI group for the AI Core SDK""" + ctx.ensure_object(dict) + ctx.obj['profile'] = profile + + +def load_service_key(service_key_json: str): + with pathlib.Path(service_key_json).open() as stream: + service_key = json.load(stream) + kwargs = {} + for value in CORE_CREDENTIAL_VALUES: + # In VCAP the service_key is nested under credentials + # We can reuse the vcap_name from the CREDENTIAL_VALUES + # when parsing the service_key + # skip if vcap_key not defined + if not value.vcap_key: + continue + try: + kwargs[value.name] = get_nested_value(service_key, value.vcap_key[1:]) + except KeyError: + kwargs[value.name] = None + return kwargs + +def get_auth_url(auth_url: Optional[str]=None): + auth_url = auth_url or prompt_for_input('Please enter the authorization URL', is_url=True, path_forbidden=False) + if auth_url.endswith(OAUTH_TOKEN_SUFFIX): + return auth_url + else: + return auth_url + OAUTH_TOKEN_SUFFIX + +def get_base_url(base_url: Optional[str]=None): + base_url = base_url or prompt_for_input('Please enter the base API URL', is_url=True, path_forbidden=False) + if base_url.endswith(API_V2_SUFFIX): + return base_url + else: + return base_url + API_V2_SUFFIX + + +def get_str_value(msg: str, value: Optional[str]=None): + return value or click.prompt(msg, type=str) + +def confirm_resource_group(resource_group: Optional[str]=None): + if resource_group is None: + resource_group = click.prompt('Please confirm or enter the AICore resource group', default=DEFAULT_RESOURCE_GROUP, type=str) + return resource_group + +def get_profile_config_path(profile: str): + profile = profile if profile != DEFAULT_PROFILE and profile is not None else None + home = pathlib.Path(get_home()) + home.mkdir(parents=True, exist_ok=True) + config_path = home / (DEFAULT_CONFIG if profile is None else f'config_{profile.lower()}.json') + if profile is not None: + click.echo(f'Remember to set `{AI_CORE_PREFIX}_PROFILE={profile.lower()}` to use your profile.') + return config_path + + +def create_config_file(config_path: pathlib.Path, auth_url: str, client_id: str, client_secret: str, + cert_file_path: pathlib.Path, key_file_path: pathlib.Path, base_url: str, resource_group: str): + config = create_config(auth_url=auth_url, client_id=client_id, client_secret=client_secret, + cert_file_path=cert_file_path, key_file_path=key_file_path, base_url=base_url, + resource_group=resource_group) + if config_path.exists() and not click.confirm(f'A config file {config_path} already exists. Do you want to replace it?'): + exit() + click.echo(f'Creating new config {config_path}') + with config_path.open('w') as stream: + json.dump(config, stream, indent=4) + + +@cli.command() +@click.option('-a', '--auth-url', default=None, type=str) +@click.option('-s', '--client-secret', default=None, type=str) +@click.option('-i', '--client-id', default=None, type=str) +@click.option('-cf', '--cert-file-path', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.option('-kf', '--key-file-path', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.option('-u', '--base-url', default=None, type=str) +@click.option('-g', '--resource-group', default=None, type=str) +@click.option('-k', '--service-key-json', default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.pass_context +def configure(ctx, auth_url, client_secret, client_id, cert_file_path, key_file_path, base_url, resource_group, + service_key_json): + profile = ctx.obj['profile'] + if service_key_json: + service_key_json = load_service_key(service_key_json) + else: + service_key_json = {} + + base_url = get_base_url(service_key_json.get('base_url', base_url)) + auth_url = get_auth_url(service_key_json.get('auth_url', auth_url)) + cert_url = service_key_json.get('cert_url', None) + if cert_url: + auth_url = get_auth_url(cert_url) + client_id = get_str_value('Please enter the client ID', service_key_json.get('client_id', client_id)) + + client_secret = service_key_json.get('client_secret', client_secret) + cert_file_path = service_key_json.get('cert_file_path', cert_file_path) + key_file_path = service_key_json.get('key_file_path', key_file_path) + + if not (client_secret is not None or (key_file_path is not None or cert_file_path is not None)): + client_secret = get_str_value( + 'Please enter the client secret (skip, if you\'re going to provide X.509 credentials', + service_key_json.get('client_secret', client_secret)) + cert_file_path = get_str_value('Please enter path to the X.509 certificate file', + service_key_json.get('cert_file_path', cert_file_path)) + key_file_path = get_str_value('Please enter path to the X.509 key file', + service_key_json.get('key_file_path', key_file_path)) + resource_group = confirm_resource_group(resource_group) + create_config_file( + config_path=get_profile_config_path(profile), + auth_url=auth_url, + client_id=client_id, + client_secret=client_secret, + cert_file_path=cert_file_path, + key_file_path=key_file_path, + base_url=base_url, + resource_group=resource_group + ) + + + +if __name__ == "__main__": + cli() #pylint: disable = no-value-for-parameter diff --git a/packages/core/ai_core_sdk/credentials.py b/packages/core/ai_core_sdk/credentials.py new file mode 100644 index 0000000..ca3874d --- /dev/null +++ b/packages/core/ai_core_sdk/credentials.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +from typing import Any, Dict, Final, List, Optional, Callable, Tuple +import json +import os +import pathlib + +from dataclasses import dataclass + +from ai_core_sdk.helpers import get_home +from ai_core_sdk.helpers.constants import (AI_CORE_PREFIX, AUTH_ENDPOINT_SUFFIX, CONFIG_FILE_ENV_VAR, PROFILE_ENV_VAR, + VCAP_AICORE_SERVICE_NAME, VCAP_SERVICES_ENV_VAR) +from ai_core_sdk.helpers.logging import get_logger + +logger = get_logger() + + +def get_nested_value(data_dict, keys: List[str]): + """ + Retrieve a nested value from a dictionary using a list of strings. + + :param data_dict: The dictionary to search. + :param keys: A list of strings representing nested keys. + :return: The value associated with the nested keys, or None if not found. + """ + current_value = data_dict + for key in keys: + current_value = current_value[key] + return current_value + + +@dataclass +class VCAPEnvironment: + services: List[Service] + + @classmethod + def from_env(cls, env_var: Optional[str] = None): + env_var = env_var or VCAP_SERVICES_ENV_VAR + env = json.loads(os.environ.get(env_var, '{}')) + return cls.from_dict(env) + + @classmethod + def from_dict(cls, env: Dict[str, Any]): + services = [Service(service) for services in env.values() for service in services] + return cls(services=services) + + def __getitem__(self, name) -> Service: + return self.get_service(name, exactly_one=True) + + def get_service(self, label, exactly_one: bool = True) -> Service: + services = [s for s in self.services if s.label == label] + if exactly_one: + if len(services) == 0: + raise KeyError(f"No service found with label '{label}'.") + return services[0] + else: + return services + + def get_service_by_name(self, name, exactly_one: bool = True) -> Service: + services = [s for s in self.services if s.name == name] + if exactly_one: + if len(services) == 0: + raise KeyError(f"No service found with name '{name}'.") + return services[0] + else: + return services + + +class _NoDefault: + def __repr__(self): + return "NoDefault" + + +NoDefault = _NoDefault() + + +class Service: + + def __init__(self, env: Dict[str, Any]): + self._env = env + + @property + def label(self) -> Optional[str]: + return self._env.get('label') + + @property + def name(self) -> Optional[str]: + return self._env.get('name') + + def __getitem__(self, key): + return self.get(key) + + def get(self, key, default=NoDefault): + if isinstance(key, str): + key_splitted = key.split('.') + else: + key_splitted = key + try: + return get_nested_value(self._env, key_splitted) or default + except KeyError: + if default is NoDefault: + raise KeyError(f"Key '{key}' not found in service '{self.name}'.") + return default + + +@dataclass +class CredentialsValue: + name: str + vcap_key: Optional[Tuple[str, ...]] = None + transform_fn: Optional[Callable] = None + + def __repr__(self): + fn = self.transform_fn.__name__ if self.transform_fn else None + return f"CredentialsValue(name={self.name!r}, vcap_key={self.vcap_key!r}, transform_fn={fn})" + + +@dataclass +class Source: + name: str + get: Callable[[CredentialsValue], Optional[str]] + + +CORE_CREDENTIAL_VALUES: Final[List[CredentialsValue]] = [ + CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid')), + CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret')), + CredentialsValue(name='auth_url', + vcap_key=('credentials', 'url'), + transform_fn=lambda url: url.rstrip('/') + + ('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)), + CredentialsValue(name='base_url', + vcap_key=('credentials', 'serviceurls', 'AI_API_URL'), + transform_fn=lambda url: url.rstrip('/') + ('' if url.endswith('/v2') else '/v2')), + CredentialsValue(name='resource_group'), + CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'), + transform_fn=lambda url: url.rstrip('/') + + ('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)), + # Even though the certificate and key in VCAP_SERVICES are not file paths, the names are defined this way in order + # to keep it compatible with the config names. It'll be handled in fetch_credentials function. + CredentialsValue(name='cert_file_path'), + CredentialsValue(name='key_file_path'), + CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'), + transform_fn=lambda cert_str: cert_str.replace('\\n', '\n')), + CredentialsValue(name='key_str', vcap_key=('credentials', 'key'), + transform_fn=lambda key_str: key_str.replace('\\n', '\n')) +] + + +def init_conf(profile: str = None): + # Read configuration from ${AICORE_HOME}/config_.json. + home = pathlib.Path(get_home()) + profile = profile or os.environ.get(PROFILE_ENV_VAR) + profile_config_file = f'config_{profile}.json' + direct_config_file = pathlib.Path(os.getenv(CONFIG_FILE_ENV_VAR)) if os.getenv(CONFIG_FILE_ENV_VAR) else None + path_to_config = (direct_config_file or + (home / ('config.json' if profile in ('default', '', None) else profile_config_file))) + config = {} + if path_to_config.exists(): + logger.debug('Config file path %s', path_to_config) + try: + with path_to_config.open(encoding='utf-8') as f: + return json.load(f) + except json.decoder.JSONDecodeError: + raise KeyError(f'{path_to_config} is not a valid json file. Please fix or remove it!') + except PermissionError as e: + logger.warning("Permission denied when trying to read config file '%s'. File ignored.", path_to_config) + return config + elif profile: + raise FileNotFoundError(f"Unable to locate profile config file '{profile_config_file}' " + f"in AICORE_HOME '{home}')") + return config + + +def _extract_credentials(source: Source, credential_values: List[CredentialsValue], exclude: List[str] = None) \ + -> Dict[str, str]: + """Extract all credentials from a source.""" + exclude = exclude or [] + credentials = {} + for cv in credential_values: + if cv.name in exclude: + continue + if value := source.get(cv): + credentials[cv.name] = cv.transform_fn(value) if cv.transform_fn else value + return credentials + + +def _resolve_credentials(sources: List[Source], credential_values: List[CredentialsValue]) -> Dict[str, str]: + """Extract credentials from the first source that has any defined.""" + for source in sources: + if credentials := _extract_credentials(source, exclude=['resource_group'], credential_values=credential_values): + logger.debug(f"Using credentials from: {source.name}") + return credentials + raise ValueError("No credentials found in any source") + + +def resolve_resource_group(sources: List[Source]) -> Optional[str]: + """Find resource_group from the first source that defines it.""" + rg_cred = CredentialsValue(name='resource_group') + for source in sources: + if value := source.get(rg_cred): + logger.debug("Using resource_group '%s' from: %s", value, source.name) + return value + logger.debug("No resource_group found in any source") + return None + + +def validate_credentials(credentials: Dict[str, str]) -> None: + """Validate that we have a complete authentication method.""" + required_base = {'client_id', 'auth_url', 'base_url'} + + # Check which auth method we have + has_client_secret = 'client_secret' in credentials + has_cert_files = 'cert_file_path' in credentials and 'key_file_path' in credentials + has_cert_strings = 'cert_str' in credentials and 'key_str' in credentials + + # Must have exactly one auth method + auth_methods = sum([has_client_secret, has_cert_files, has_cert_strings]) + + if auth_methods == 0: + raise ValueError( + "No authentication method found. Must provide one of:\n" + "1. client_secret\n" + "2. cert_file_path AND key_file_path\n" + "3. cert_str AND key_str" + ) + + if auth_methods > 1: + raise ValueError( + "Multiple authentication methods found. Please provide only one of:\n" + "1. client_secret\n" + "2. cert_file_path AND key_file_path\n" + "3. cert_str AND key_str" + ) + + # Check required base fields + missing = required_base - set(credentials.keys()) + if missing: + raise ValueError(f"Missing required credentials: {missing}") + + +def _str_or_none(value) -> Optional[str]: + return str(value) if value else None + + +def fetch_credentials(profile: str = None, credential_values: List[CredentialsValue] = CORE_CREDENTIAL_VALUES, + validate: bool = True, **kwargs) -> Dict[str, str]: + """ + Fetch credentials from a single source based on precedence. + + Precedence order: kwargs > environment variables > config file > VCAP service + + Once a source is selected (first one with any credential), all credentials + come from that source only. Resource group is an exception and follows + precedence independently. + + If credential_values is provided and it's not extended from the CORE_CREDENTIAL_VALUES, set validate to False + """ + config = init_conf(profile=profile) + + try: + vcap_service = VCAPEnvironment.from_env()[VCAP_AICORE_SERVICE_NAME] + except KeyError: + vcap_service = None + + sources = [ + Source("kwargs", + lambda cv: _str_or_none(kwargs.get(cv.name))), + Source("environment variables", + lambda cv: _str_or_none(os.environ.get(f'{AI_CORE_PREFIX}_{cv.name.upper()}'))), + Source("config file", + lambda cv: _str_or_none(config.get(f'{AI_CORE_PREFIX}_{cv.name.upper()}'))), + Source("VCAP service", + lambda cv: _str_or_none(vcap_service.get(cv.vcap_key, None) if vcap_service and cv.vcap_key else None)), + ] + + credentials = _resolve_credentials(sources, credential_values) + + # Use cert_url as auth_url if present (VCAP provides cert_url for certificate auth) + if 'cert_url' in credentials: + credentials['auth_url'] = credentials.pop('cert_url') + + if validate: + validate_credentials(credentials) + + resource_group = resolve_resource_group(sources) + if resource_group: + credentials['resource_group'] = resource_group + + return credentials diff --git a/packages/core/ai_core_sdk/exception.py b/packages/core/ai_core_sdk/exception.py new file mode 100644 index 0000000..7ed71c6 --- /dev/null +++ b/packages/core/ai_core_sdk/exception.py @@ -0,0 +1,11 @@ +from ai_api_client_sdk.exception import AIAPIAuthenticatorException, AIAPIAuthorizationException, \ + AIAPIInvalidRequestException, AIAPINotFoundException, AIAPIPreconditionFailedException, \ + AIAPIServerException + + +class AICoreSDKException(Exception): + """Base Exception class for AI Core SDK exceptions""" + + +class AICoreInvalidInputException(AICoreSDKException): + """Exception thrown in case of invalid input""" diff --git a/packages/core/ai_core_sdk/helpers/__init__.py b/packages/core/ai_core_sdk/helpers/__init__.py new file mode 100644 index 0000000..0b87ffa --- /dev/null +++ b/packages/core/ai_core_sdk/helpers/__init__.py @@ -0,0 +1,39 @@ +import os +from typing import Dict + +from ai_api_client_sdk.helpers.authenticator import Authenticator +from .constants import DEFAULT_HOME_PATH, HOME_PATH_ENV_VAR + + +def form_top_skip_params(top: int = None, skip: int = None) -> Dict[str, int]: + """ + Frame query param + + :param top: Number of objects to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of objects to be skipped, from the list of the queried objects, + defaults to None + :type skip: int, optional + """ + params = {} + if top: + params['$top'] = top + if skip: + params['$skip'] = skip + if not params: + params = None + return params + + +def is_within_aicore() -> bool: + """[summary] + Function to check whether the sdk is used within or out of aicore cluster + Returns: + bool: True if the ai-core-sdk is used within aicore cluster + False if the ai-core-sdk is used outside aicore cluster + """ + return os.getenv('AICORE_EXECUTION_ID') and os.getenv('AICORE_TRACKING_ENDPOINT') + + +def get_home() -> str: + return os.environ.get(HOME_PATH_ENV_VAR, DEFAULT_HOME_PATH) diff --git a/packages/core/ai_core_sdk/helpers/constants.py b/packages/core/ai_core_sdk/helpers/constants.py new file mode 100644 index 0000000..7091330 --- /dev/null +++ b/packages/core/ai_core_sdk/helpers/constants.py @@ -0,0 +1,18 @@ +import os +from enum import Enum + +AI_CORE_PREFIX = 'AICORE' +AUTH_ENDPOINT_SUFFIX = '/oauth/token' +CONFIG_FILE_ENV_VAR = f'{AI_CORE_PREFIX}_CONFIG' +DEBUG_ENV_VAR_NAME = "DEBUG" +DEFAULT_HOME_PATH = os.path.join(os.path.expanduser('~'), '.aicore') +HOME_PATH_ENV_VAR = f'{AI_CORE_PREFIX}_HOME' +PROFILE_ENV_VAR = f'{AI_CORE_PREFIX}_PROFILE' +VCAP_AICORE_SERVICE_NAME = 'aicore' +VCAP_SERVICES_ENV_VAR = 'VCAP_SERVICES' + + +class Timeouts(Enum): + READ_TIMEOUT = 60 + CONNECT_TIMEOUT = 60 + NUM_REQUEST_RETRIES = 3 \ No newline at end of file diff --git a/packages/core/ai_core_sdk/helpers/logging.py b/packages/core/ai_core_sdk/helpers/logging.py new file mode 100644 index 0000000..1a4ad35 --- /dev/null +++ b/packages/core/ai_core_sdk/helpers/logging.py @@ -0,0 +1,23 @@ +import logging +import os + +from ai_core_sdk.helpers.constants import DEBUG_ENV_VAR_NAME + +BASE_LOGGER_NAME = "ai_core_sdk" +DEFAULT_LOG_LEVEL = logging.INFO + + +def get_logger(name: str = None): + # Use a hierarchical logger structure to allow for more granular control + logger_name = f"{name}" if name else BASE_LOGGER_NAME + return logging.getLogger(logger_name) + + +def set_log_level(logger: logging.Logger, default_level=DEFAULT_LOG_LEVEL): + # Check if DEBUG is set to "true" (case-insensitive) + debug_env = os.getenv(DEBUG_ENV_VAR_NAME) + debug = debug_env is not None and debug_env.lower() == 'true' + logger.setLevel(logging.DEBUG if debug else default_level) + + +set_log_level(get_logger()) diff --git a/packages/core/ai_core_sdk/models/__init__.py b/packages/core/ai_core_sdk/models/__init__.py new file mode 100644 index 0000000..b146ddd --- /dev/null +++ b/packages/core/ai_core_sdk/models/__init__.py @@ -0,0 +1,33 @@ +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.base_models import ( + BasicResponse, + KeyValue, + Name, + NameValue, + Order, + QueryResponse, +) +from ai_api_client_sdk.models.configuration import Configuration +from ai_api_client_sdk.models.deployment import Deployment +from ai_api_client_sdk.models.executable import Executable +from ai_api_client_sdk.models.execution import Execution +from ai_api_client_sdk.models.healthz_status import HealthzStatus +from ai_api_client_sdk.models.input_artifact import InputArtifact +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_label import MetricLabel +from ai_api_client_sdk.models.metric_resource import MetricResource +from ai_api_client_sdk.models.metric_tag import MetricTag +from ai_api_client_sdk.models.metrics_query_response import MetricsQueryResponse +from ai_api_client_sdk.models.model import Model +from ai_api_client_sdk.models.model_query_response import ModelQueryResponse +from ai_api_client_sdk.models.model_version import ModelVersion +from ai_api_client_sdk.models.output_artifact import OutputArtifact +from ai_api_client_sdk.models.parameter import Parameter +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.models.scenario import Scenario +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.target_status import TargetStatus +from ai_api_client_sdk.models.version import Version diff --git a/packages/core/ai_core_sdk/models/application.py b/packages/core/ai_core_sdk/models/application.py new file mode 100644 index 0000000..a79c625 --- /dev/null +++ b/packages/core/ai_core_sdk/models/application.py @@ -0,0 +1,37 @@ +from typing import Dict + + +class Application: + """The Application object defines the application. + + :param path: path within the repository + :type path: str + :param revision: revision + :type revision: str + :param repository_url: URL of the repository + :type repository_url: str + :param application_name: name of the application + :type application_name: str + """ + + # pylint:disable=W0613 + def __init__(self, path: str, revision: str, repository_url: str, application_name: str, **kwargs): + self.path: str = path + self.revision: str = revision + self.repository_url: str = repository_url + self.application_name: str = application_name + + def __str__(self): + return "Application name: " + str(self.application_name) + + @staticmethod + def from_dict(application_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.application.Application` object, created from the values in + the dict provided as parameter + + :param application_dict: Dict which includes the necessary values to create the object + :type application_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.application.Application` + """ + return Application(**application_dict) diff --git a/packages/core/ai_core_sdk/models/application_query_response.py b/packages/core/ai_core_sdk/models/application_query_response.py new file mode 100644 index 0000000..5689e08 --- /dev/null +++ b/packages/core/ai_core_sdk/models/application_query_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.application import Application + + +class ApplicationQueryResponse(QueryResponse): + """The ApplicationQueryResponse object defines the response of the applications query request + :param resources: List of the applications returned from the server + :type resources: List[class:`ai_core_sdk.models.application.Application`] + :param count: Total number of the queried applications + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Application], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse` + """ + response_dict['resources'] = [Application.from_dict(r) for r in response_dict['resources']] + return ApplicationQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/models/application_resource_sync_status.py b/packages/core/ai_core_sdk/models/application_resource_sync_status.py new file mode 100644 index 0000000..ada3a81 --- /dev/null +++ b/packages/core/ai_core_sdk/models/application_resource_sync_status.py @@ -0,0 +1,34 @@ +from typing import Dict + + +class ApplicationResourceSyncStatus: + """The ApplicationSyncResourcesStatus object defines the status of sync of application resource. + + :param name: Name of the application resource, defaults to None + :type name: str, optional + :param kind: kind of the application resource + :type kind: str, optional + :param status: status of the sync of the application resource + :type status: str, optional + :param message: application resource message + :type message: str, optional + """ + + def __init__(self, name: str = None, kind: str = None, status: str = None, message: str = None, **kwargs): + self.name: str = name + self.kind: str = kind + self.status: str = status + self.message: str = message + + @staticmethod + def from_dict(app_res_sync_status_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus` + object, created from the values in the dict provided as parameter + + :param app_res_sync_status_dict: Dict which includes the necessary values to create the object + :type app_res_sync_status_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus` + """ + return ApplicationResourceSyncStatus(**app_res_sync_status_dict) + diff --git a/packages/core/ai_core_sdk/models/application_source.py b/packages/core/ai_core_sdk/models/application_source.py new file mode 100644 index 0000000..b7050bf --- /dev/null +++ b/packages/core/ai_core_sdk/models/application_source.py @@ -0,0 +1,34 @@ +from typing import Dict + + +class ApplicationSource: + """The ApplicationSource object defines the application source. + + :param repo_url: URL of the repository, defaults to None + :type repo_url: str, optional + :param path: path within the repository, defaults to None + :type path: str, optional + :param revision: revision number of the application, defaults to None + :type revision: str, optional + """ + + # pylint:disable=W0613 + def __init__(self, repo_url: str = None, path: str = None, revision: str = None, **kwargs): + self.repourl: str = repo_url + self.path: str = path + self.revision: str = revision + + def __str__(self): + return "ApplicationSource repourl: " + str(self.repourl) + ", ApplicationSource revision: " + str(self.revision) + + @staticmethod + def from_dict(application_source_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.application_source.ApplicationSource` object, created from the values in + the dict provided as parameter + + :param application_source_dict: Dict which includes the necessary values to create the object + :type application_source_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.application_source.ApplicationSource` + """ + return ApplicationSource(**application_source_dict) diff --git a/packages/core/ai_core_sdk/models/application_status.py b/packages/core/ai_core_sdk/models/application_status.py new file mode 100644 index 0000000..3530ec1 --- /dev/null +++ b/packages/core/ai_core_sdk/models/application_status.py @@ -0,0 +1,66 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models.application_source import ApplicationSource +from ai_core_sdk.models.application_resource_sync_status import ApplicationResourceSyncStatus + + +class ApplicationStatus: + """The Application object defines the application. + + :param health_status: Application health status, defaults to None + :type health_status: str, optional + :param sync_status: Application sync status, defaults to None + :type sync_status: str, optional + :param message: Application health status message, defaults to None + :type message: str, optional + :param source: Application source, defaults to None + :type source: class:`ai_core_sdk.models.application_status.ApplicationStatus`, optional + :param sync_finished_at: Application sync finish time, defaults to None + :type sync_finished_at: str, optional + :param sync_started_at: Application sync start time, defaults to None + :type sync_started_at: str, optional + :param reconciled_at: Application reconciliation time, defaults to None + :type reconciled_at: str, optional + :param sync_resources_status: Status of the synchronization of the application resources, defaults to None + :type sync_resources_status: + List[class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus`], optional + """ + + def __init__(self, health_status: str = None, sync_status: str = None, message: str = None, + source: ApplicationSource = None, sync_finished_at: str = None, sync_started_at: str = None, + reconciled_at: str = None, sync_resources_status: List[ApplicationResourceSyncStatus] = None, + **kwargs): + self.health_status: str = health_status + self.sync_status: str = sync_status + self.message: str = message + self.source: ApplicationSource = source + self.sync_finished_at: str = sync_finished_at + self.sync_started_at: str = sync_started_at + self.reconciled_at: str = reconciled_at + self.sync_resources_status: List[ApplicationResourceSyncStatus] = sync_resources_status + # sync_ressources_status property is deprecated, please use sync_resources_status instead + self.sync_ressources_status: List[ApplicationResourceSyncStatus] = self.sync_resources_status + + def __str__(self): + return "ApplicationStatus health status: " + str(self.health_status) + \ + ", ApplicationStatus sync status: " + str(self.sync_status) + \ + ", ApplicationStatus message: " + str(self.message) + ", " + str(self.source) + + @staticmethod + def from_dict(application_status_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.application_status.ApplicationStatus` object, created from the values in + the dict provided as parameter + + :param application_status_dict: Dict which includes the necessary values to create the object + :type application_status_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.application_status.ApplicationStatus` + """ + if 'source' in application_status_dict: + application_status_dict['source'] = ApplicationSource.from_dict(application_status_dict['source']) + if 'sync_resources_status' in application_status_dict: + application_status_dict['sync_resources_status'] = [ + ApplicationResourceSyncStatus.from_dict(asd) + for asd in application_status_dict['sync_resources_status'] + ] + return ApplicationStatus(**application_status_dict) diff --git a/packages/core/ai_core_sdk/models/base_models.py b/packages/core/ai_core_sdk/models/base_models.py new file mode 100644 index 0000000..70ac0e6 --- /dev/null +++ b/packages/core/ai_core_sdk/models/base_models.py @@ -0,0 +1,62 @@ +from typing import Dict + + +class BasicNameResponse: + """The BasicNameResponse object defines the response with name from the server + + :param name: Name of the relevant resource + :type id: str + :param message: Response message from the server + :type message: str + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, name: str, message: str, **kwargs): + self.name: str = name + self.message: str = message + + def __str__(self): + return "Name: " + str(self.name) + ", Message: " + str(self.message) + + @staticmethod + def from_dict(bnr_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.base_models.BasicNameResponse` object, created from the values in the + dict provided as parameter + + :param bnr_dict: Dict which includes the necessary values to create the object + :type bnr_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse` + """ + return BasicNameResponse(**bnr_dict) + + +class Message: + """Message object defines a message + + :param message: message + :type message: str + """ + + def __init__(self, message: str, **kwargs): + self.message: str = message + + def __eq__(self, other): + if not isinstance(other, Message): + return False + return self.message == other.message + + def __str__(self): + return "Message: " + str(self.message) + + @staticmethod + def from_dict(message_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.base_models.Message` object, created from the values in the + dict provided as parameter + + :param message_dict: Dict which includes the necessary values to create the object + :type message_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.base_models.Message` + """ + return Message(**message_dict) diff --git a/packages/core/ai_core_sdk/models/docker_registry_secret.py b/packages/core/ai_core_sdk/models/docker_registry_secret.py new file mode 100644 index 0000000..03da362 --- /dev/null +++ b/packages/core/ai_core_sdk/models/docker_registry_secret.py @@ -0,0 +1,23 @@ +from typing import Dict + +from ai_core_sdk.models import Name + + +class DockerRegistrySecret(Name): + """The DockerRegistrySecret object defines the docker registry secret. Refer to + :class:`ai_api_client_sdk.models.base_models.Name`, for the object definition + """ + def __str__(self): + return "DockerRegistrySecret name: " + str(self.name) + + @staticmethod + def from_dict(docker_registry_secret_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret` object, created + from the values in the dict provided as parameter + + :param docker_registry_secret_dict: Dict which includes the necessary values to create the object + :type docker_registry_secret_dict: Dict[str, str] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret` + """ + return DockerRegistrySecret(**docker_registry_secret_dict) diff --git a/packages/core/ai_core_sdk/models/docker_registry_secret_query_response.py b/packages/core/ai_core_sdk/models/docker_registry_secret_query_response.py new file mode 100644 index 0000000..52bcfd7 --- /dev/null +++ b/packages/core/ai_core_sdk/models/docker_registry_secret_query_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.docker_registry_secret import DockerRegistrySecret + + +class DockerRegistrySecretQueryResponse(QueryResponse): + """The DockerRegistrySecretQueryResponse object defines the response of the dockerRegistrySecrets query request + :param resources: List of the docker registry secrets returned from the server + :type resources: List[class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret`] + :param count: Total number of the queried docker registry secrets + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[DockerRegistrySecret], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + """ + response_dict['resources'] = [DockerRegistrySecret.from_dict(r) for r in response_dict['resources']] + return DockerRegistrySecretQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/models/kpi.py b/packages/core/ai_core_sdk/models/kpi.py new file mode 100644 index 0000000..d0d6511 --- /dev/null +++ b/packages/core/ai_core_sdk/models/kpi.py @@ -0,0 +1,25 @@ +from typing import Any, Dict, List, Union + + +class Kpi: + """The Kpi object defines the Kpi data. + """ + + def __init__(self, header: List[str], rows: List[Union[str, int]], **kwargs): + self.header: List[str] = header + self.rows: List[Union[str, int]] = rows + + def __str__(self): + return "KPIs header(s): " + ', '.join(self.header) + + @staticmethod + def from_dict(kpi_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.kpi.Kpi` object, created + from the values in the dict provided as parameter + + :param kpi_dict: Dict which includes the necessary values to create the object + :type kpi_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.kpi.Kpi` + """ + return Kpi(**kpi_dict) diff --git a/packages/core/ai_core_sdk/models/object_store_secret.py b/packages/core/ai_core_sdk/models/object_store_secret.py new file mode 100644 index 0000000..31d046f --- /dev/null +++ b/packages/core/ai_core_sdk/models/object_store_secret.py @@ -0,0 +1,32 @@ +from typing import Any, Dict + + +class ObjectStoreSecret: + """The ObjectStoreSecret object defines the object store secret response. + """ + + def __init__(self, name: str, metadata: Dict[str, str], **kwargs): + key_prefix = 'storage.ai.sap.com/' + path_prefix_key = f'{key_prefix}path_prefix' + self.name: str = name + self.metadata: Dict[str, str] = metadata + # pathPrefix key is getting converted into snake case during object mapping d + # The below code converts path prefix from snake case to camel case + if path_prefix_key in metadata: + self.metadata[f'{key_prefix}pathPrefix'] = metadata[path_prefix_key] + del metadata[path_prefix_key] + + def __str__(self): + return "Object store secret name: " + str(self.name) + + @staticmethod + def from_dict(object_store_secret_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret` object, created + from the values in the dict provided as parameter + + :param object_store_secret_dict: Dict which includes the necessary values to create the object + :type object_store_secret_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret` + """ + return ObjectStoreSecret(**object_store_secret_dict) diff --git a/packages/core/ai_core_sdk/models/object_store_secret_query_response.py b/packages/core/ai_core_sdk/models/object_store_secret_query_response.py new file mode 100644 index 0000000..8c8d97e --- /dev/null +++ b/packages/core/ai_core_sdk/models/object_store_secret_query_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.object_store_secret import ObjectStoreSecret + + +class ObjectStoreSecretQueryResponse(QueryResponse): + """The ObjectStoreSecretQueryResponse object defines the response of the object store secret query request + :param resources: List of the object store secrets returned from the server + :type resources: List[class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret`] + :param count: Total number of the queried object store secrets + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[ObjectStoreSecret], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse` + """ + response_dict['resources'] = [ObjectStoreSecret.from_dict(r) for r in response_dict['resources']] + return ObjectStoreSecretQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/models/repository.py b/packages/core/ai_core_sdk/models/repository.py new file mode 100644 index 0000000..ebcbcf3 --- /dev/null +++ b/packages/core/ai_core_sdk/models/repository.py @@ -0,0 +1,36 @@ +from typing import Dict + +from ai_core_sdk.models.repository_status import RepositoryStatus + + +class Repository: + """The Repository object defines the repository + + :param name: name of the repository + :type name: str + :param url: URL of the repository + :type url: str + :param status: status of the repository, defaults to None + :type status: class:`ai_core_sdk.models.repository_status.RepositoryStatus`, optional + """ + def __init__(self, name: str, url: str, status: RepositoryStatus = None, **kwargs): + self.name: str = name + self.url: str = url + self.status: RepositoryStatus = status + + def __str__(self): + return "Repository name: " + str(self.name) + ", Repository url: " + str(self.url) + + @staticmethod + def from_dict(repository_dict: Dict[str, str]): + """Returns a :class:`ai_core_sdk.models.repository.Repository` object, created + from the values in the dict provided as parameter + + :param repository_dict: Dict which includes the necessary values to create the object + :type repository_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.repository.Repository` + """ + if 'status' in repository_dict: + repository_dict['status'] = RepositoryStatus(repository_dict['status']) + return Repository(**repository_dict) diff --git a/packages/core/ai_core_sdk/models/repository_query_response.py b/packages/core/ai_core_sdk/models/repository_query_response.py new file mode 100644 index 0000000..3b7a3b2 --- /dev/null +++ b/packages/core/ai_core_sdk/models/repository_query_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.repository import Repository + + +class RepositoryQueryResponse(QueryResponse): + """The RepositoryQueryResponse object defines the response of the repository query request + :param resources: List of the repositories returned from the server + :type resources: List[class:`ai_core_sdk.models.repository.Repository`] + :param count: Total number of the queried repositories + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Repository], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_client_sdk.models.repository_query_response.RepositoryQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.repository_query_response.RepositoryQueryResponse` + """ + response_dict['resources'] = [Repository.from_dict(r) for r in response_dict['resources']] + return RepositoryQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/models/repository_status.py b/packages/core/ai_core_sdk/models/repository_status.py new file mode 100644 index 0000000..a913614 --- /dev/null +++ b/packages/core/ai_core_sdk/models/repository_status.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class RepositoryStatus(Enum): + """RepositoryStatus is an Enum defining the valid values of the status of a repository + """ + ERROR = 'ERROR' + IN_PROGRESS = 'IN-PROGRESS' + COMPLETED = 'COMPLETED' diff --git a/packages/core/ai_core_sdk/models/resource_group.py b/packages/core/ai_core_sdk/models/resource_group.py new file mode 100644 index 0000000..2ff40ce --- /dev/null +++ b/packages/core/ai_core_sdk/models/resource_group.py @@ -0,0 +1,50 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import Label +from ai_core_sdk.models.resource_group_status import ResourceGroupStatus + + +class ResourceGroup: + """ResourceGroup represents the resource group. + + :param resource_group_id: The resource_group_id of this ResourceGroup. + :type resource_group_id: str + :param tenant_id: The tenant_id of this ResourceGroup. + :type tenant_id: str + :param zone_id: The zone_id of this ResourceGroup. + :type zone_id: str + :param labels: The labels of this ResourceGroup. + :type labels: ResourceGroupLabels + :param status: The status of this ResourceGroup. + :type status: str + :param status_message: The status_message of this ResourceGroup. + :type status_message: str + """ + def __init__(self, resource_group_id: str = None, tenant_id: str = None, zone_id: str = None, + labels: List[Label] = None, status: ResourceGroupStatus = None, status_message: str = None, **kwargs): + self.resource_group_id: str = resource_group_id + self.tenant_id: str = tenant_id + self.zone_id: str = zone_id + self.labels: List[Label] = labels + self.status: ResourceGroupStatus = status + self.status_message: str = status_message + + def __str__(self): + return "Resource group id: " + str(self.resource_group_id) + + @staticmethod + def from_dict(resource_group_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.resource_group.ResourceGroup` object, created + from the values in the dict provided as parameter + + :param resource_group_dict: Dict which includes the necessary values to create the object + :type resource_group_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup` + """ + if 'resource_group_status' in resource_group_dict: + resource_group_dict['resource_group_status'] = \ + ResourceGroupStatus(resource_group_dict['resource_group_status']) + if 'labels' in resource_group_dict: + resource_group_dict['labels'] = [Label.from_dict(l) for l in resource_group_dict['labels']] + return ResourceGroup(**resource_group_dict) diff --git a/packages/core/ai_core_sdk/models/resource_group_query_response.py b/packages/core/ai_core_sdk/models/resource_group_query_response.py new file mode 100644 index 0000000..d9df804 --- /dev/null +++ b/packages/core/ai_core_sdk/models/resource_group_query_response.py @@ -0,0 +1,31 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.resource_group import ResourceGroup + + +class ResourceGroupQueryResponse(QueryResponse): + """The ResourceGroupQueryResponse object defines the response of the resourceGroups query request + :param resources: List of the resource groups returned from the server + :type resources: List[class:`ai_core_sdk.models.resource_group.ResourceGroup`] + :param count: Total number of the queried docker registry secrets + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + + def __init__(self, resources: List[ResourceGroup], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + """ + response_dict['resources'] = [ResourceGroup.from_dict(r) for r in response_dict['resources']] + return ResourceGroupQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/models/resource_group_status.py b/packages/core/ai_core_sdk/models/resource_group_status.py new file mode 100644 index 0000000..1407f09 --- /dev/null +++ b/packages/core/ai_core_sdk/models/resource_group_status.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ResourceGroupStatus(Enum): + """ResourceGroupStatus is an Enum defining the valid values of the status of a resource group + """ + ERROR = 'ERROR' + PROVISIONED = 'PROVISIONED' + PROVISIONING = 'PROVISIONING' diff --git a/packages/core/ai_core_sdk/models/secret.py b/packages/core/ai_core_sdk/models/secret.py new file mode 100644 index 0000000..420c039 --- /dev/null +++ b/packages/core/ai_core_sdk/models/secret.py @@ -0,0 +1,30 @@ +from typing import Any, Dict + + +class Secret: + """The Secret object defines the secret response. + + :param name: Secret name + :type name: str + :param data: Secret data dictionary, defaults to None + :type data: dict, optional + """ + + def __init__(self, name: str, data: Dict[str, str] = None, **kwargs): + self.name: str = name + self.data: Dict[str, str] = data + + def __str__(self): + return "Secret name: " + str(self.name) + + @staticmethod + def from_dict(secret_dict: Dict[str, Any]): + """Returns a :class:`ai_core_sdk.models.secret.Secret` object, created + from the values in the dict provided as parameter + + :param secret_dict: Dict which includes the necessary values to create the object + :type secret_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.secret.Secret` + """ + return Secret(**secret_dict) diff --git a/packages/core/ai_core_sdk/models/secret_query_response.py b/packages/core/ai_core_sdk/models/secret_query_response.py new file mode 100644 index 0000000..7243243 --- /dev/null +++ b/packages/core/ai_core_sdk/models/secret_query_response.py @@ -0,0 +1,30 @@ +from typing import Any, Dict, List + +from ai_core_sdk.models import QueryResponse +from ai_core_sdk.models.secret import Secret + + +class SecretQueryResponse(QueryResponse): + """The SecretQueryResponse object defines the response of the secret query request + :param resources: List of the secrets returned from the server + :type resources: List[class:`ai_core_sdk.models.secret.Secret`] + :param count: Total number of the queried secrets + :type count: int + :param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server + """ + def __init__(self, resources: List[Secret], count: int, **kwargs): + super().__init__(resources=resources, count=count, **kwargs) + + @staticmethod + def from_dict(response_dict: Dict[str, Any]): + """Returns a + :class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse` + object, created from the values in the dict provided as parameter + + :param response_dict: Dict which includes the necessary values to create the object + :type response_dict: Dict[str, Any] + :return: An object, created from the values provided + :rtype: class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse` + """ + response_dict['resources'] = [Secret.from_dict(r) for r in response_dict['resources']] + return SecretQueryResponse(**response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/__init__.py b/packages/core/ai_core_sdk/resource_clients/__init__.py new file mode 100644 index 0000000..69ea87d --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/__init__.py @@ -0,0 +1,13 @@ +from ai_api_client_sdk.ai_api_v2_client import AIAPIV2Client +from ai_api_client_sdk.helpers.rest_client import RestClient +from ai_api_client_sdk.resource_clients.artifact_client import ArtifactClient +from ai_api_client_sdk.resource_clients.base_client import BaseClient +from ai_api_client_sdk.resource_clients.configuration_client import ConfigurationClient +from ai_api_client_sdk.resource_clients.deployment_client import DeploymentClient +from ai_api_client_sdk.resource_clients.executable_client import ExecutableClient +from ai_api_client_sdk.resource_clients.execution_client import ExecutionClient +from ai_api_client_sdk.resource_clients.meta_client import MetaClient +from ai_api_client_sdk.resource_clients.metrics_client import MetricsClient +from ai_api_client_sdk.resource_clients.model_client import ModelClient +from ai_api_client_sdk.resource_clients.resource_groups_client import ResourceGroupsClient +from ai_api_client_sdk.resource_clients.scenario_client import ScenarioClient diff --git a/packages/core/ai_core_sdk/resource_clients/applications_client.py b/packages/core/ai_core_sdk/resource_clients/applications_client.py new file mode 100644 index 0000000..908a887 --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/applications_client.py @@ -0,0 +1,173 @@ +from ai_core_sdk.exception import AICoreInvalidInputException +from ai_core_sdk.models import BasicResponse +from ai_core_sdk.models.application import Application +from ai_core_sdk.models.application_query_response import ApplicationQueryResponse +from ai_core_sdk.models.application_status import ApplicationStatus +from ai_core_sdk.resource_clients import BaseClient + + +class ApplicationsClient(BaseClient): + """ApplicationsClient is a class implemented for interacting with the applications related + endpoints of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + + __PATH = '/admin/applications' + + def create(self, revision: str, path: str, application_name: str = None, repository_name: str = None, + repository_url: str = None) -> BasicResponse: + """Creates an application. + + :param revision: revision to synchronize + :type revision: str + :param path: within the repository to synchronize + :type path: str + :param application_name: Name of the application + :type application_name: str, optional + :param repository_name: Name of the repository to synchronize. Either this or the repository_url needs to be + provided + :type repository_name: str, optional + :param repository_url: URL of the repository to synchronize. Either this or the repository_name needs to be + provided + :type repository_url: str, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + if (repository_url and repository_name) or (not repository_name and not repository_url): + raise AICoreInvalidInputException('Either repository_url or repository_name must be provided, not both') + body = {'revision': revision, 'path': path} + if repository_name: + body['repository_name'] = repository_name + elif repository_url: + body['repository_url'] = repository_url + if application_name: + body['application_name'] = application_name + response_dict = self.rest_client.post(path=self.__PATH, body=body) + return BasicResponse.from_dict(response_dict) + + def delete(self, application_name: str) -> BasicResponse: + """Deletes the application. + + :param application_name: name of the application to be deleted + :type application_name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{application_name}') + return BasicResponse.from_dict(response_dict) + + def get(self, application_name: str) -> Application: + """Retrieves the application from the server. + + :param application_name: name of the application to be retrieved + :type application_name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved application + :rtype: class:`ai_core_sdk.models.application.Application` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{application_name}') + return Application.from_dict(response_dict) + + def get_status(self, application_name: str) -> ApplicationStatus: + """Retrieves the application status from the server. + + :param application_name: name of the application + :type application_name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved application status + :rtype: class:`ai_core_sdk.models.application_status.ApplicationStatus` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{application_name}/status') + return ApplicationStatus.from_dict(response_dict) + + def modify(self, application_name: str, repository_url: str, path: str, revision: str) -> BasicResponse: + """Modifies the application + + :param application_name: name of the application to be modified + :type name: str + :param repository_url: + :type repository_url: str + :param revision: + :type revision: str + :param path: + :type path: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {'path': path, 'revision': revision, 'repository_url': repository_url} + response_dict = self.rest_client.patch(path=f'{self.__PATH}/{application_name}', body=body) + return BasicResponse.from_dict(response_dict) + + def query(self) -> ApplicationQueryResponse: + """Returns the applications. + + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved applications + :rtype: class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse` + """ + response_dict = self.rest_client.get(path=self.__PATH) + return ApplicationQueryResponse.from_dict(response_dict) + + def refresh(self, application_name: str) -> BasicResponse: + """Triggers synchronisation of the application. + + :param application_name: name of the application to be refreshed + :type application_name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.post(path=f'{self.__PATH}/{application_name}/refresh') + return BasicResponse.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/docker_registry_secrets_client.py b/packages/core/ai_core_sdk/resource_clients/docker_registry_secrets_client.py new file mode 100644 index 0000000..bdf5ba1 --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/docker_registry_secrets_client.py @@ -0,0 +1,117 @@ +from ai_core_sdk.helpers import form_top_skip_params +from ai_core_sdk.models import BasicResponse +from ai_core_sdk.models.base_models import Message +from ai_core_sdk.models.docker_registry_secret import DockerRegistrySecret +from ai_core_sdk.models.docker_registry_secret_query_response import DockerRegistrySecretQueryResponse +from ai_core_sdk.resource_clients import BaseClient + + +class DockerRegistrySecretsClient(BaseClient): + """DockerRegistrySecretsClient is a class implemented for interacting with the docker registry secret related + endpoints of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/admin/dockerRegistrySecrets' + + def create(self, name: str, data: dict) -> Message: + """Creates a docker secret based on the configuration in the request body. + + :param name: name of the docker registry secret + :type name: str + :param data: json dict, defining the docker registry secret + :type data: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_core_sdk.models.base_models.Message` + """ + body = {'name': name, 'data': data} + response_dict = self.rest_client.post(path=f'{self.__PATH}', body=body) + return Message.from_dict(response_dict) + + def delete(self, name: str) -> BasicResponse: + """Deletes the docker registry secret with the given name if it exists. + + :param name: name of the docker registry secret to be deleted + :type name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{name}') + return BasicResponse.from_dict(response_dict) + + def get(self, name: str) -> DockerRegistrySecret: + """Returns the metadata of the docker registry secrets which matches the given name. + + :param name: name of the docker registry secret to be retrieved + :type name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved metadata of the docker registry secret + :rtype: class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{name}') + return DockerRegistrySecret.from_dict(response_dict) + + def modify(self, name: str, data: dict) -> BasicResponse: + """Updates the docker registry secret + + :param name: name of the docker registry secret to be modified + :type name: str + :param data: json dict, defining the docker registry secret + :type data: dict + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {'data': data} + response_dict = self.rest_client.patch(path=f'{self.__PATH}/{name}', body=body) + return BasicResponse.from_dict(response_dict) + + def query(self, top: int = None, skip: int = None) -> DockerRegistrySecretQueryResponse: + """Gets a list of metadata of docker registry secrets. + + :param top: Number of docker registry secrets to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of docker registry secrets to be skipped, from the list of the queried docker registry + secrets, defaults to None + :type skip: int, optional + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of metadata of secrets + :rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse` + """ + params = form_top_skip_params(top, skip) + response_dict = self.rest_client.get(path=f'{self.__PATH}', params=params) + return DockerRegistrySecretQueryResponse.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/internal_rest_client.py b/packages/core/ai_core_sdk/resource_clients/internal_rest_client.py new file mode 100644 index 0000000..91879ce --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/internal_rest_client.py @@ -0,0 +1,85 @@ +import os +from ai_api_client_sdk.ai_api_v2_client import RestClient + +from ai_core_sdk.exception import AICoreSDKException +from ai_core_sdk.helpers import is_within_aicore + + +class InternalRestClient(RestClient): + """InternalRestClient is a class implemented for sending requests to services within aicore. The InternalRestClient should only be used for services that do not require authentication when called within aicore. + """ + + def __init__( + self, + base_url=None, + get_token=None, + resource_group=None, + *args, + **kwargs, + ): + # Disallows parameters set by this class. This way this class must not be adjusted as the signiture of its parent class changes. + if base_url or get_token or resource_group: + raise AICoreSDKException( + "InternalRestClient should must not be called with base_url, get_token or resource_group" + ) + + if not is_within_aicore(): + raise AICoreSDKException( + "Attempted to skip authentication even though SDK is not used within aicore" + ) + + api_base_url = os.getenv("AICORE_TRACKING_ENDPOINT") + + # framing the base url of tracking endpoint + base_url = f"{api_base_url}/api/v1" + + # dummy token creator function to be passed to rest client + def dummy_token_creator(): + return "" + + token_creator = dummy_token_creator + + # resource group will be set as a header in the _handle_request method + resource_group = "" + + self.tenant = os.getenv("AI-MAIN-TENANT") + self.resource_group = os.getenv("AI-RESOURCE-GROUP") + + super().__init__( + base_url=base_url, + get_token=token_creator, + resource_group=resource_group, + *args, + **kwargs, + ) + + def _handle_request( + self, + method: str, + path: str, + params=None, + body_json=None, + headers=None, + resource_group=None, + return_bytes_content=False, + convert_body_to_camel_case=True, + **kwargs + ): + headers = headers or {} + + headers.update({ + "AI-MAIN-TENANT": self.tenant, + "AI-RESOURCE-GROUP": self.resource_group, + }) + + return super()._handle_request( + method=method, + path=path, + params=params, + body_json=body_json, + headers=headers, + resource_group=resource_group, + return_bytes_content=return_bytes_content, + convert_body_to_camel_case=convert_body_to_camel_case, + **kwargs + ) diff --git a/packages/core/ai_core_sdk/resource_clients/kpi_client.py b/packages/core/ai_core_sdk/resource_clients/kpi_client.py new file mode 100644 index 0000000..835bb2c --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/kpi_client.py @@ -0,0 +1,26 @@ +from ai_core_sdk.models.kpi import Kpi +from ai_core_sdk.resource_clients import BaseClient + + +class KpiClient(BaseClient): + """KpiClient is a class implemented for interacting with the analytics kpi + endpoint of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/analytics/kpis' + + def query(self) -> Kpi: + """Retrieves the number of executions, artifacts, and deployments + for each resource group, scenario, and executable. + + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved KPI data + :rtype: class:`ai_core_sdk.models.kpi.Kpi` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}') + return Kpi.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/metrics_client.py b/packages/core/ai_core_sdk/resource_clients/metrics_client.py new file mode 100644 index 0000000..3168767 --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/metrics_client.py @@ -0,0 +1,138 @@ +from typing import List + +from ai_core_sdk.models import Metric, MetricCustomInfo, MetricTag, MetricLabel +from ai_core_sdk.resource_clients import MetricsClient + +from ai_core_sdk.exception import AICoreSDKException +class MetricsCoreClient(MetricsClient): + """MetricsCoreClient is a class implemented for interacting with the metrics related + endpoints of the server. It is inherited from the base class + :class:`ai_api_client_sdk.resource_clients.metrics_client.MetricsClient` + """ + + def __init__(self, rest_client, execution_id: str = None) -> None: + super().__init__(rest_client) + self.execution_id = execution_id + self.rest_client = rest_client + self.metrics_path = '/metrics' + + def _resolve_execution_id(self, execution_id: str) -> str: + return execution_id or self.execution_id + + def modify(self, execution_id: str = '', metrics: List[Metric] = None, tags: List[MetricTag] = None, + custom_info: List[MetricCustomInfo] = None, resource_group: str = None) -> None: + """Creates or updates the metrics for an execution. + + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param metrics: List of the metrics related to the execution, defaults to None + :type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional + :param tags: List of the tags related to the execution, defaults to None + :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional + :param custom_info: List of custom info related to the execution, defaults to None + :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + execution_id = self._resolve_execution_id(execution_id) + body = {'execution_id': execution_id} + if metrics: + body['metrics'] = [metric.to_dict() for metric in metrics] + if tags: + body['tags'] = [tag.__dict__ for tag in tags] + if custom_info: + body['custom_info'] = [c_info.__dict__ for c_info in custom_info] + + self.rest_client.patch(path=self.metrics_path, body=body, resource_group=resource_group) + + def log_metrics(self, metrics: List[Metric], execution_id: str = '', artifact_name: str = None, + resource_group: str = None) -> None: + """Creates or updates the metrics for an execution. + + :param metrics: List of the metrics related to the execution, + :type metrics: List[class:`ai_api_client_sdk.metric.Metric`] + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param artifact_name: Name of the artifact to associate with a metric, defaults to None + :type artifact_name: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + execution_id = self._resolve_execution_id(execution_id) + body = {'execution_id': execution_id} + if artifact_name is not None: + if not isinstance(artifact_name, str): + raise AICoreSDKException('Artifact name of type string is expected') + + artifact_label = { + 'name': "metrics.ai.sap.com/Artifact.name", + 'value': artifact_name + } + + for metric in metrics: + if not metric.labels: + metric.labels = [MetricLabel.from_dict(artifact_label)] + else: + metric.labels.append(MetricLabel.from_dict(artifact_label)) + if metrics: + body['metrics'] = [metric.to_dict() for metric in metrics] + self.rest_client.patch(path=self.metrics_path, body=body, resource_group=resource_group) + + def set_custom_info(self, custom_info: List[MetricCustomInfo], execution_id: str = '', + resource_group: str = None) -> None: + """log custom info against the given execution + captures consumption semantics for the metrics or complex metric in JSON format. + + + :param custom_info: List of custom info related to the execution + :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + execution_id = self._resolve_execution_id(execution_id) + body = {'execution_id': execution_id} + if custom_info: + body['custom_info'] = [c_info.__dict__ for c_info in custom_info] + self.rest_client.patch(path=self.metrics_path, body=body, resource_group=resource_group) + + def set_tags(self, tags: List[MetricTag], execution_id: str = '', resource_group: str = None) -> None: + """log tags against the given execution + + :param tags: List of the tags related to the execution, defaults to None + :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'] + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + execution_id = self._resolve_execution_id(execution_id) + body = {'execution_id': execution_id} + if tags: + body['tags'] = [tag.__dict__ for tag in tags] + self.rest_client.patch(path=self.metrics_path, body=body, resource_group=resource_group) diff --git a/packages/core/ai_core_sdk/resource_clients/object_store_secrets_client.py b/packages/core/ai_core_sdk/resource_clients/object_store_secrets_client.py new file mode 100644 index 0000000..bb8bb99 --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/object_store_secrets_client.py @@ -0,0 +1,215 @@ +from ai_core_sdk.helpers import form_top_skip_params +from ai_core_sdk.models import BasicResponse +from ai_core_sdk.models.base_models import Message +from ai_core_sdk.models.object_store_secret import ObjectStoreSecret +from ai_core_sdk.models.object_store_secret_query_response import ObjectStoreSecretQueryResponse +from ai_core_sdk.resource_clients import BaseClient + + +class ObjectStoreSecretsClient(BaseClient): + """ObjectStoreSecretsClient is a class implemented for interacting with the object store secret related + endpoints of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/admin/objectStoreSecrets' + + def create(self, name: str, type: str, data: dict, bucket: str = None, endpoint: str = None, region: str = None, + path_prefix: str = None, verifyssl: str = None, usehttps: str = None, + resource_group: str = None) -> Message: + """Creates an object store secret. + + :param name: name of the object store secret + :type name: str + :param type: type of object storage + :type type: str + :param data: data to be posted + :type data: str + :param bucket: name of the bucket + :type bucket: str + :param endpoint: endpoint of object storage + :type endpoint: str + :param region: region of object storage + :type region: str + :param path_prefix: path prefix + :type path_prefix: str + :param verifyssl: verify ssl + :type verifyssl: str + :param usehttps: use https + :type usehttps: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIConflictException` if a 409 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of metadata of available secrets + :rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse` + """ + body = { + 'name': name, + 'type': type, + 'data': data, + } + + if bucket: + body['bucket'] = bucket + if endpoint: + body['endpoint'] = endpoint + if region: + body['region'] = region + if path_prefix: + body['path_prefix'] = path_prefix + if verifyssl: + body['verifyssl'] = verifyssl + if usehttps: + body['usehttps'] = usehttps + + response_dict = self.rest_client.post(path=f'{self.__PATH}', body=body, resource_group=resource_group) + return Message.from_dict(response_dict) + + def delete(self, name: str, resource_group: str = None) -> BasicResponse: + """Deletes the object store secret. + + :param name: name of the object store secret to be deleted + :type name: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{name}', resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def get(self, name: str, resource_group: str = None) -> ObjectStoreSecret: + """Retrieves the object store secret from the server. + + :param name: name of the object store secret to be retrieved + :type name: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The retrieved object store secret + :rtype: class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{name}', resource_group=resource_group) + return ObjectStoreSecret.from_dict(response_dict) + + def modify(self, name: str, type: str, data: dict, bucket: str = None, endpoint: str = None, region: str = None, + path_prefix: str = None, verifyssl: str = None, usehttps: str = None, + resource_group: str = None) -> BasicResponse: + """Modifies the object store secret + + :param name: name of the object store secret to be modified + :type name: str + :param type: type of object storage + :type type: str + :param data: data to be posted + :type data: str + :param bucket: name of the bucket + :type bucket: str + :param endpoint: endpoint of object storage + :type endpoint: str + :param region: region of object storage + :type region: str + :param path_prefix: path prefix + :type path_prefix: str + :param verifyssl: verify ssl + :type verifyssl: str + :param usehttps: use https + :type usehttps: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = { + 'name': name, + 'type': type, + 'data': data, + } + + if bucket: + body['bucket'] = bucket + if endpoint: + body['endpoint'] = endpoint + if region: + body['region'] = region + if path_prefix: + body['path_prefix'] = path_prefix + if verifyssl: + body['verifyssl'] = verifyssl + if usehttps: + body['usehttps'] = usehttps + + response_dict = self.rest_client.patch(path=f'{self.__PATH}/{name}', body=body, resource_group=resource_group) + return BasicResponse.from_dict(response_dict) + + def query(self, top: int = None, skip: int = None, resource_group: str = None) -> ObjectStoreSecretQueryResponse: + """Returns the object store secrets. + + :param top: Number of object store secrets to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of object store secrets to be skipped, from the list of the queried object store + secrets, defaults to None + :type skip: int, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of object store secrets + :rtype: class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse` + """ + params = form_top_skip_params(top, skip) + response_dict = self.rest_client.get(path=f'{self.__PATH}', params=params, resource_group=resource_group) + return ObjectStoreSecretQueryResponse.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/repositories_client.py b/packages/core/ai_core_sdk/resource_clients/repositories_client.py new file mode 100644 index 0000000..a37faef --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/repositories_client.py @@ -0,0 +1,116 @@ +from ai_core_sdk.models import BasicResponse +from ai_core_sdk.models.base_models import Message +from ai_core_sdk.models.repository import Repository +from ai_core_sdk.models.repository_query_response import RepositoryQueryResponse +from ai_core_sdk.resource_clients import BaseClient + + +class RepositoriesClient(BaseClient): + """RepositoriesClient is a class implemented for interacting with the repositories related + endpoints of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/admin/repositories' + + def create(self, name: str, url: str, username: str, password: str) -> Message: + """On-boards a new GitOps repository + + :param name: name of the GitOps repository + :type name: str + :param url: url of the GitOps repository + :type url: str + :param username: username to the GitOps repository + :type username: str + :param password: password to the GitOps repository + :type password: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse` + """ + body = {'name': name, 'url': url, 'username': username, 'password': password} + response_dict = self.rest_client.post(path=self.__PATH, body=body) + return Message.from_dict(response_dict) + + def delete(self, name: str) -> BasicResponse: + """Off-boards a GitOps repository. + + :param name: name of the repository to be deleted + :type name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{name}') + return BasicResponse.from_dict(response_dict) + + def get(self, name: str) -> Repository: + """Retrieves the access details for a repository if it exists. + + :param name: name of the repository to be retrieved + :type name: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: The access details for a repository + :rtype: class:`ai_core_client_sdk.models.docker_registry_secret.DockerRegistrySecret` + """ + response_dict = self.rest_client.get(path=f'{self.__PATH}/{name}') + return Repository.from_dict(response_dict) + + def modify(self, name: str, username: str, password: str) -> BasicResponse: + """Updates the referenced repository credentials to synchronize repository. + + :param name: name of the repository to be modified + :type name: str + :param username: username to the repository + :type username: str + :param password: password to the repository + :type password: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse` + """ + body = {'username': username, 'password': password} + response_dict = self.rest_client.patch(path=f'{self.__PATH}/{name}', body=body) + return BasicResponse.from_dict(response_dict) + + def query(self) -> RepositoryQueryResponse: + """Retrieves a list of all GitOps repositories for a tenant. + + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list all GitOps repositories for a tenant + :rtype: class:`ai_core_client_sdk.models.repository_query_response.RepositoryQueryResponse` + """ + response_dict = self.rest_client.get(path=self.__PATH) + return RepositoryQueryResponse.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/resource_clients/secrets_client.py b/packages/core/ai_core_sdk/resource_clients/secrets_client.py new file mode 100644 index 0000000..086b35a --- /dev/null +++ b/packages/core/ai_core_sdk/resource_clients/secrets_client.py @@ -0,0 +1,148 @@ +from ai_core_sdk.helpers import form_top_skip_params +from ai_core_sdk.models import BasicResponse +from ai_core_sdk.models.base_models import Message +from ai_core_sdk.models.secret_query_response import SecretQueryResponse +from ai_core_sdk.resource_clients import BaseClient + + +class SecretsClient(BaseClient): + """SecretsClient is a class implemented for interacting with the secret related + endpoints of the server. It implements the base class + :class:`ai_api_client_sdk.resource_clients.base_client.BaseClient` + """ + __PATH = '/admin/secrets' + + def create(self, name: str, data: dict, resource_group: str = None, ai_tenant_scope=True) -> Message: + """Creates a secret. + + :param name: name of the secret + :type name: str + :param data: data of secret + :type data: dict + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :param ai_tenant_scope: Specify whether the main tenant scope is to be used + :type ai_tenant_scope: bool + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIConflictException` if a 409 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of metadata of available secrets + :rtype: class:`ai_core_sdk.models.base_models.Message` + """ + body = { + 'name': name, + 'data': data, + } + headers = {'AI-Tenant-Scope': str(ai_tenant_scope).lower()} + + response_dict = self.rest_client.post(path=f'{self.__PATH}', body=body, resource_group=resource_group, + headers=headers) + return Message.from_dict(response_dict) + + def delete(self, name: str, resource_group: str = None, ai_tenant_scope=True) -> Message: + """Deletes the secret. + + :param name: name of the secret to be deleted + :type name: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :param ai_tenant_scope: Specify whether the main tenant scope is to be used + :type ai_tenant_scope: bool + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.Message` + """ + headers = {'AI-Tenant-Scope': str(ai_tenant_scope).lower()} + + response_dict = self.rest_client.delete(path=f'{self.__PATH}/{name}', resource_group=resource_group, + headers=headers) + if response_dict == 200: + response_dict = { "message": "Secret has been deleted" } + return Message.from_dict(response_dict) + + def modify(self, name: str, data: dict, resource_group: str = None, ai_tenant_scope=True) -> Message: + """Modifies the secret. + + :param name: name of the secret to be modified + :type name: str + :param data: data of secret + :type data: dict + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :param ai_tenant_scope: Specify whether the main tenant scope is to be used + :type ai_tenant_scope: bool + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from + the server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.base_models.Message` + """ + body = { + 'data': data, + } + headers = {'AI-Tenant-Scope': str(ai_tenant_scope).lower()} + + response_dict = self.rest_client.patch(path=f'{self.__PATH}/{name}', body=body, resource_group=resource_group, + headers=headers) + return Message.from_dict(response_dict) + + def query(self, top: int = None, skip: int = None, resource_group: str = None, + ai_tenant_scope: bool = True) -> SecretQueryResponse: + """Returns the secrets. + + :param top: Number of secrets to be retrieved, defaults to None + :type top: int, optional + :param skip: Number of secrets to be skipped, from the list of the queried secrets, defaults to None + :type skip: int, optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified, + defaults to None + :type resource_group: str + :param ai_tenant_scope: Specify whether the main tenant scope is to be used + :type ai_tenant_scope: bool + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: A list of secrets + :rtype: class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse` + """ + params = form_top_skip_params(top, skip) + headers = {'AI-Tenant-Scope': str(ai_tenant_scope).lower()} + + response_dict = self.rest_client.get(path=f'{self.__PATH}', params=params, headers=headers, + resource_group=resource_group) + return SecretQueryResponse.from_dict(response_dict) diff --git a/packages/core/ai_core_sdk/tracking/__init__.py b/packages/core/ai_core_sdk/tracking/__init__.py new file mode 100644 index 0000000..cb89c0e --- /dev/null +++ b/packages/core/ai_core_sdk/tracking/__init__.py @@ -0,0 +1,2 @@ +# pylint: disable=C0114 +from ai_core_sdk.tracking.tracking import Tracking diff --git a/packages/core/ai_core_sdk/tracking/tracking.py b/packages/core/ai_core_sdk/tracking/tracking.py new file mode 100644 index 0000000..85680f5 --- /dev/null +++ b/packages/core/ai_core_sdk/tracking/tracking.py @@ -0,0 +1,215 @@ +# pylint: disable=C0114 +import os +from typing import Callable, List + +from ai_api_client_sdk.helpers.authenticator import Authenticator +from ai_core_sdk.exception import AIAPIAuthenticatorException +from ai_core_sdk.helpers import is_within_aicore +from ai_core_sdk.models import Metric, MetricCustomInfo, MetricsQueryResponse, MetricTag +from ai_core_sdk.resource_clients import AIAPIV2Client +from ai_core_sdk.resource_clients.internal_rest_client import InternalRestClient +from ai_core_sdk.resource_clients.metrics_client import MetricsCoreClient + + +class Tracking(MetricsCoreClient): # pylint: disable=W0223 + """Tracking is a class implemented for interacting with the metrics related + endpoints of the server. It is a wrapper around the base class + :class:`ai_core_sdk.resource_clients.metrics_client.MetricsCoreClient` + + :param base_url: Base URL of the AI Core. Should include the base path as well. (i.e., "/lm/scenarios" + should work) + :type base_url: str + :param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to + None + :type auth_url: str, optional + :param client_id: client id to be used for authorization, defaults to None + :type client_id: str, optional + :param client_secret: client secret to be used for authorization, defaults to None + :type client_secret: str, optional + :param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None + :type cert_str: str, optional + :param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None + :type key_str: str, optional + :param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter, + defaults to None + :type cert_file_path: str, optional + :param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter, + defaults to None + :type key_file_path: str, optional + :param token_creator: the function which returns the Bearer token, when called. Either this, or + auth_url & client_id & client_secret should be specified, defaults to None + :type token_creator: Callable[[], str], optional + :param resource_group: The default resource group which will be used while sending the requests to the server. If + not set, the resource_group should be specified with every request to the server, defaults to None + :type resource_group: str, optional + """ + def __init__(self, base_url: str = None, auth_url: str = None, client_id: str = None, client_secret: str = None, # pylint: disable=W0231,R0913 + cert_str: str = None, key_str: str = None, cert_file_path: str = None, key_file_path: str = None, + token_creator: Callable[[], str] = None, resource_group: str = None): + self.base_url: str = base_url + self.metrics_path = '/metrics' + self.local_experimentation = False + ai_api_base_url = f'{base_url}/lm' + + if base_url: + token_creator = AIAPIV2Client._create_token_creator_if_does_not_exist( + token_creator=token_creator, auth_url=auth_url, client_id=client_id, client_secret=client_secret, + cert_str=cert_str, key_str=key_str, cert_file_path=cert_file_path, key_file_path=key_file_path) + ai_api_v2_client = AIAPIV2Client(base_url=ai_api_base_url, token_creator=token_creator, + resource_group=resource_group) + self.metrics_core_client = MetricsCoreClient(rest_client = ai_api_v2_client.rest_client) + elif is_within_aicore(): + self.metrics_core_client = MetricsCoreClient( + rest_client=InternalRestClient(), + execution_id=os.getenv("AICORE_EXECUTION_ID"), + ) + else: + print('Warning: Enabling local experimentation. Metrics logged will not be persisted anywhere.') + self.local_experimentation = True + + def modify(self, execution_id: str = '', metrics: List[Metric] = None, tags: List[MetricTag] = None, # pylint: disable=R0913 + custom_info: List[MetricCustomInfo] = None, resource_group: str = None) -> None: + """Creates or updates the metrics for an execution. + + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param metrics: List of the metrics related to the execution, defaults to None + :type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional + :param tags: List of the tags related to the execution, defaults to None + :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional + :param custom_info: List of custom info related to the execution, defaults to None + :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + if not self.local_experimentation: + self.metrics_core_client.modify(execution_id=execution_id, metrics=metrics, tags=tags, + custom_info=custom_info, resource_group=resource_group) + + def log_metrics(self, metrics: List[Metric], execution_id: str = '', artifact_name: str = None, + resource_group: str = None) -> None: + """Creates or updates the metrics for an execution. + + :param metrics: List of the metrics related to the execution, + :type metrics: List[class:`ai_api_client_sdk.metric.Metric`] + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param artifact_name: Name of the artifact to associate with a metric, defaults to None + :type artifact_name: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + if not self.local_experimentation: + self.metrics_core_client.log_metrics(metrics=metrics, + execution_id=execution_id, + artifact_name=artifact_name, + resource_group=resource_group) + + def set_custom_info(self, custom_info: List[MetricCustomInfo], execution_id: str = '', + resource_group: str = None) -> None: + """log custom info against the given execution + captures consumption semantics for the metrics or complex metric in JSON format. + + + :param custom_info: List of custom info related to the execution + :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + if not self.local_experimentation: + self.metrics_core_client.set_custom_info(custom_info=custom_info, execution_id=execution_id, + resource_group=resource_group) + + def set_tags(self, tags: List[MetricTag], execution_id: str = '', resource_group: str = None) -> None: + """log tags against the given execution + + :param tags: List of the tags related to the execution, defaults to None + :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'] + :param execution_id: ID of the execution, of which the metrics should be modified. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + if not self.local_experimentation: + self.metrics_core_client.set_tags(tags=tags, execution_id=execution_id, resource_group=resource_group) + + def query(self, filter: str = None, execution_ids: List[str] = None, # pylint: disable=W0622 + select: List[str] = None, resource_group: str = None) -> \ + MetricsQueryResponse: + """Creates or updates the metrics for an execution. + + :param filter: Deprecated. Use parameter execution_ids instead. A filter expression that filters the metric + resources using execution IDs. User can only use in, eq operators in filter expression, defaults to None + :type filter: str, optional + :param execution_ids: IDs of the executions, of which the metrics should be retrieved, defaults to None + :type execution_ids: List[str], optional + :param select: Values of select can be metrics,tags,customInfo or any of the combinations of these or *. + Can be used to select(project) only the resources specified + :type select: List[str], optional + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + :return: An object representing the response from the server + :rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse` + """ + if self.local_experimentation: + print('Warning: Response will be always empty') + return MetricsQueryResponse.from_dict({"count":0,"resources":[]}) + return self.metrics_core_client.query(filter=filter, + execution_ids=execution_ids, + select=select, + resource_group=resource_group) + + def delete(self, execution_id: str, resource_group: str = None) -> None: + """Deletes the metrics. + + :param execution_id: ID of the execution, of which the metrics should be deleted. + :type execution_id: str + :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default + resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified, + defaults to None + :type resource_group: str + :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the + server + :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the + server + """ + if not self.local_experimentation: + self.metrics_core_client.delete(execution_id=execution_id, resource_group=resource_group) diff --git a/packages/core/docs/CHANGELOG.md b/packages/core/docs/CHANGELOG.md new file mode 100644 index 0000000..63c3d4f --- /dev/null +++ b/packages/core/docs/CHANGELOG.md @@ -0,0 +1,1365 @@ +# [3.3.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.2.3...v3.3.0) (2026-03-13) + + +### Features + +* **requirements:** update sap-ai-sdk-base-version ([709dca1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/709dca101c856d1a3fae82d92b8570a5c38583b4)) + +## [3.2.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.2.2...v3.2.3) (2026-03-13) + + +### Bug Fixes + +* tracking tests ([d4a3998](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d4a3998ad1ce6972db31354b948297151c3e860b)) + +## [3.2.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.2.1...v3.2.2) (2026-03-09) + + +### Bug Fixes + +* **tests:** use .get() instead of direct accessor to get json values ([b6aed34](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b6aed3424b7b29ee15e1b81ea31df50c659394b4)) + +## [3.2.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.2.0...v3.2.1) (2026-03-05) + + +### Bug Fixes + +* **requirements:** upgrade sap-ai-sdk-base ([84ede8e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/84ede8e21b3937dea5f68bebd3b98231c5e6044e)) + +# [3.2.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.6...v3.2.0) (2026-02-23) + + +### Features + +* **credentials:** make credentials extendible ([ca46e84](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ca46e8489b06c20ad9462f319a1dc8ed45fabcef)) + +## [3.1.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.5...v3.1.6) (2026-01-29) + + +### Bug Fixes + +* ignore config file on permission error ([70c3a60](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/70c3a60cc9e837905105fb5a6217d3e058432bdf)) + +## [3.1.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.4...v3.1.5) (2026-01-21) + + +### Bug Fixes + +* **deps:** update dependency pytest to v9 ([4955db1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4955db11fce40e46ffc03d4a4dd11da4dd4cdebb)) + +## [3.1.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.3...v3.1.4) (2026-01-21) + + +### Bug Fixes + +* new base image ([4d3cc05](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4d3cc051090d204e676b7970a99d3a7c09632403)) + +## [3.1.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.2...v3.1.3) (2025-12-19) + + +### Bug Fixes + +* **docs:** aicore configure ([b6635cd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b6635cddf2621b2e223c4984f6469e3c4a7e992e)) + +## [3.1.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.1...v3.1.2) (2025-12-15) + + +### Bug Fixes + +* **image:** dont use hardcoded value ([c86f104](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c86f104102a6334cfb46fefb9dae766f68724931)) + +## [3.1.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.1.0...v3.1.1) (2025-11-28) + + +### Bug Fixes + +* set tenant id and rg id as headers for tracking requests ([c8587ee](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c8587ee2b9d1e3a4a5858ab73160837f8b697144)) +* tests ([221c315](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/221c3156f408f71145b8506226f66cf0fe2fd049)) +* tests ([625da04](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/625da04042a41520842a39ba303195c6f57350c2)) +* update as per suggestions ([ed8947c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ed8947c6375c70b8b2b1c73d38b918c6aa916edd)) +* update as per suggestions ([44b70e9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/44b70e9f9d13334bcc9098e0f75e3419b09e038a)) +* update as per suggestions ([2a864ec](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/2a864ec61dddedd71dcb372f86ab029fca4d834a)) +* update as per suggestions ([4f701c2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4f701c25c668cc4024d3e9973af39d22fa7a8d76)) + +# [3.1.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.15...v3.1.0) (2025-11-24) + + +### Bug Fixes + +* **cleanup:** move request based args to kwars ([93d5d23](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/93d5d23c5ad150b4a7ec1429b74476f8b39e65f2)) +* **client:** add client_type to doc string, use AI Core Python SDK as default ([a11d6a4](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a11d6a47a28a385bdad65f5a9cc72585ad68cef4)) +* **test:** small test fixes and adjustments ([3e9317a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3e9317a3e692766b8137d4fd550da957fbffca7d)) + + +### Features + +* **client:** support AI_CLIENT_TYPE env var ([1b07b18](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1b07b18f26cf9b7e607808247df831884998301e)) + +## [3.0.15](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.14...v3.0.15) (2025-11-19) + + +### Bug Fixes + +* **deps:** update dependency pytest-cov to v7 ([5acf4e7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5acf4e75ed10c3f1d6c15e5c3c8eb7187d5e183c)) + +## [3.0.14](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.13...v3.0.14) (2025-11-17) + + +### Bug Fixes + +* **deps:** update dependency pylint to v4 ([18d7002](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/18d7002e36fcbd82ddc0eba9ce5152d848c6a698)) + +## [3.0.13](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.12...v3.0.13) (2025-11-03) + + +### Bug Fixes + +* **deps:** update dockerio.int.repositories.cloud.sap/python docker tag to v3.14 ([1fa60b0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1fa60b0cccaa82238adf14d15f6a5421544271f3)) +* **metrics_client:** ensure passed execution_id takes precedence over instance value ([18c85f6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/18c85f6dc7b4e58de92715177f4e63228206ccb1)) +* **metrics_client:** treat empty string as explicit execution_id ([7bce850](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7bce8504a78b74973f762eba63e8680766e3279e)) + +## [3.0.12](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.11...v3.0.12) (2025-10-15) + + +### Bug Fixes + +* **requirements:** adjust requirements ([54d9713](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/54d9713806ca6242fac4ebc7a952f5cb36e95092)) + +## [3.0.11](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.10...v3.0.11) (2025-08-12) + + +### Bug Fixes + +* update version dummy ([#287](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/287)) ([6f9b9b7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6f9b9b772e6150eee9f2d4b4843091f1b7f304f2)) + +## [3.0.10](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.9...v3.0.10) (2025-07-31) + + +### Bug Fixes + +* **setup:** shift to pyhton 3.13 ([ef7ee54](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ef7ee546cac7f46a57469b902a80904e70a60dd0)) + +## [3.0.9](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.8...v3.0.9) (2025-07-14) + + +### Bug Fixes + +* **cli:** aicore command cannot be found ([3a8ed33](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3a8ed333e6889a2c5be52a283faeafdd6f74620c)) + +## [3.0.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.7...v3.0.8) (2025-07-02) + + +### Bug Fixes + +* **makefile:** set correct output dir for component tests ([7af5688](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7af568831c1ef7a8b105a4b0bdb90c79e7f7b43c)) + +## [3.0.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.6...v3.0.7) (2025-07-02) + + +### Bug Fixes + +* **cicd:** ensure test reporting is done correctly ([1991a61](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1991a618219ce1a0772ee548b38d859221c62fa2)) +* **requirements:** add pylint to requirements ([19aba4a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/19aba4a3cb5c826ed0ba24cd4bac7cb983c91977)) + +## [3.0.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.5...v3.0.6) (2025-07-02) + + +### Bug Fixes + +* **build:** re-run master ([58c5065](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/58c5065bb1659be8ad8ebda70c99b5ad84ba2879)) + +## [3.0.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.4...v3.0.5) (2025-06-26) + + +### Bug Fixes + +* **requirements:** Update requirements.txt ([b924bbe](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b924bbec52223b91850ae10d660f6f4cd8aa7106)) + +## [3.0.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.3...v3.0.4) (2025-06-11) + + +### Bug Fixes + +* **tests:** fix makefile so that sonar reports code coverage again ([53c2c09](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/53c2c09881adf19f7205109dbd7a6ebe4b143e66)) + +## [3.0.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.2...v3.0.3) (2025-06-05) + + +### Bug Fixes + +* **config:** update xmake creds ([d6084af](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d6084af3d395e348dc15bcbc081376c4be080029)) +* **requirements:** update dependencies ([ee59b2f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ee59b2fe0c70c2d619fbacac8536b764899133b5)) +* **requirements:** update requirements.txt ([bf1bf3a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/bf1bf3a2175d35e5486bd6f613d0ae786b3b4177)) + +## [3.0.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.1...v3.0.2) (2025-06-05) + + +### Bug Fixes + +* **dev:** fix pipeline ([c966c55](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c966c55b458dde95db296ae0fd735810e648926d)) + +## [3.0.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v3.0.0...v3.0.1) (2025-05-30) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.41 ([9d243bd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9d243bd361e17ba4b83b71ccc95526d07b1c1956)) + +# [3.0.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.6.2...v3.0.0) (2025-05-15) + + +### Features + +* **package-name:** rebranding-AIWDF2526 ([f08963b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f08963bb039438d8bba538e60f5753ab0bd51c95)) +* **package-name:** rebranding-AIWDF2526 ([67a5899](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/67a5899710894c8d54750397793f4acad5a87d16)) +* **package-name:** rebranding-AIWDF2526 ([25ce6c5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/25ce6c54b11a7b135e5b07db4edad948816d1d5e)) +* **package-name:** rebranding-AIWDF2526 ([f315670](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f315670cb141e9f2a6a5cb33f1f9a1eee92cddee)) +* **package-name:** rebranding-AIWDF2526 ([db113a3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/db113a3a113f2c9ca43038c0eee0c5dbd9d7181f)) + + +### BREAKING CHANGES + +* **package-name:** package name was changed to sap-ai-sdk-core +* **package-name:** package name was changed to sap-ai-sdk-core + +## [2.6.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.6.1...v2.6.2) (2025-05-14) + + +### Bug Fixes + +* **dependencies:** client sdk AIWDF2525 ([ff8542e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ff8542e7ec9eed31f6d43a169d962e206dbff6a8)) + +## [2.6.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.6.0...v2.6.1) (2025-04-29) + + +### Bug Fixes + +* **dependencies:** ai-api-client version ([f1dada2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f1dada242e2b81d1581a707595c10298f08ace22)) + +# [2.6.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.11...v2.6.0) (2025-04-29) + + +### Bug Fixes + +* **requirements:** update client sdk ([20b3b85](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/20b3b85ac139c4568def7ecca335a8191cdeb8c5)) + + +### Features + +* **deprecation:** rebranding-AIWDF2025 ([ca71d11](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ca71d11e254d13dd71a8e903d7ddd9e981710302)) + +## [2.5.11](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.10...v2.5.11) (2025-04-27) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.24 ([2c1e291](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/2c1e291db6a52d4df3b58a0179e4a0b1a853c81b)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.32 ([9cece68](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9cece6897fbdce51baa5b499d2f6ea7cb3eb4439)) + +## [2.5.10](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.9...v2.5.10) (2025-04-01) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.22 ([f925e0e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f925e0e037255118a1c2e4dda6d79445dfb5a9e1)) + +## [2.5.9](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.8...v2.5.9) (2025-03-27) + + +### Bug Fixes + +* **blackduck:** add blackduck ctp scan ([450769f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/450769f29f6bc131df20576d80d5e4d3c43599d5)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.15 ([5561647](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/55616479a62c0ace7743b9766ba06d22aff67275)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.17 ([dd56fbb](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dd56fbb5aac1ea0e998ff862827a60f9080ad8f6)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.18 ([6b8e08c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6b8e08ca8652f2fe81e4c69631ac470a18f95bbd)) +* **template:** fix overwritten changes ([c501b2b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c501b2bab3c157e0c69c9661d4803fade750360f)) +* **template:** use new registry ([778f38a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/778f38aeba991c9d5d9c11eccc1007746c0efa9a)) +* **tests:** fix metrics e2e tests ([8cb9009](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8cb9009f7b0454bc3173fd02e64d7fe097ddef41)) +* **tests:** fix metrics e2e tests ([5293149](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/529314971879532d5cfc5871142afd1957f4f697)) +* **tests:** fix metrics e2e tests ([7cee05f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7cee05fb0ab120ef3573698381b7384aeb47a947)) + +## [2.5.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.7...v2.5.8) (2025-02-17) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.20.9 ([3363edc](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3363edc054a0b2c99ee3eb1d4fe3d976918c1e70)) + +## [2.5.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.6...v2.5.7) (2025-02-04) + + +### Bug Fixes + +* **metadata:** add required intended audience ([fd32bae](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fd32bae83919a48a567725d3b5d860fcc051d410)) + +## [2.5.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.5...v2.5.6) (2025-01-28) + + +### Bug Fixes + +* **client-sdk:** rest client AIWDF2214 ([08bb4c0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/08bb4c0e68e106bff2b0678b2bcb163121890357)) + +## [2.5.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.4...v2.5.5) (2025-01-28) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.5 ([c22ea95](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c22ea95be8dddd2f1e5f31bf92f4db387f9dd753)) + +## [2.5.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.3...v2.5.4) (2025-01-21) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.4 ([791b262](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/791b26237dc766ef300f979ced736bba615eef3a)) + +## [2.5.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.2...v2.5.3) (2025-01-17) + + +### Bug Fixes + +* **docker-registry:** new docker registry for tests AIWDF-2207 ([bd6a647](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/bd6a64766b1fa6126db8ca1ef7d6cbf95e0074be)) + +## [2.5.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.1...v2.5.2) (2025-01-16) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.17.2 ([d541a57](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d541a571c5f81594bfc527707611ad55cd5c5e72)) +* **docker-registry:** new docker registry for tests AIWDF-2207 ([b511347](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b51134744e41ebbe08836c3a665f3c6c64a16701)) + +## [2.5.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.5.0...v2.5.1) (2024-12-26) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.14.7 ([649c3ba](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/649c3baa4fd74089e6843515ed479330ebc58eef)) + +# [2.5.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.18...v2.5.0) (2024-12-19) + + +### Features + +* update wft with cluster names ([8f2480f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8f2480fff48c75c1c0fdb988f66579a3e76ec72b)) + +## [2.4.18](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.17...v2.4.18) (2024-12-16) + + +### Bug Fixes + +* **docker-registry:** create docker secret ([8606536](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/86065364269e5d1d602019ceb447fa68cda134d8)) +* **docker-registry:** new docker registry for tests ([21f34ff](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/21f34ff8a8fa6297271223834b752f0843ba5a35)) + +## [2.4.17](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.16...v2.4.17) (2024-12-15) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.14.5 ([a98e455](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a98e455a0ea2a1274adf8ab3e9df6d0c62655b33)) + +## [2.4.16](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.15...v2.4.16) (2024-12-10) + + +### Bug Fixes + +* **client-sdk:** update ai-api-client-sdk ([5f676e8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5f676e8c6eb7aba9256d023155e5eada9999da79)) + +## [2.4.15](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.14...v2.4.15) (2024-12-04) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.33 ([fbf2897](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fbf289751de6f8ed77b8dd30efc5b3c6930641cb)) + +## [2.4.14](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.13...v2.4.14) (2024-11-19) + + +### Bug Fixes + +* **auth:** add loggings to differentiate the source of access configurations ([61491c5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/61491c5fdfeac92d572f94e5c3d9d22afdd48270)) +* **deps:** update dependency pytest-cov to v6 ([9f59df6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9f59df67331ef5c7b6ea218a533b8ea23c69244e)) + +## [2.4.13](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.12...v2.4.13) (2024-11-13) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.32 ([177e488](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/177e488da3946843e18a2ea0a91dd6e50b367b72)) + +## [2.4.12](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.11...v2.4.12) (2024-10-24) + + +### Bug Fixes + +* **deps:** update ai-api-client-sdk ([1443384](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/144338480c757cc7f64c377b2c43f861ebda21ea)) + +## [2.4.11](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.10...v2.4.11) (2024-10-21) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.28 ([e49b660](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e49b6603cda16d8320d950a839b433f8e013cd94)) + +## [2.4.10](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.9...v2.4.10) (2024-10-18) + + +### Bug Fixes + +* **deps:** update ai-api-client-sdk to v2.3.0 ([098d1f6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/098d1f614df3e2ab96c70206522ad5b9aafe4a8e)) + +## [2.4.9](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.8...v2.4.9) (2024-10-10) + + +### Bug Fixes + +* **checkmarx:** switch to checkmarx one ([d203187](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d203187934bda0821b6151ab3213a30ee93abb11)) +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.22 ([adaf121](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/adaf12191065cfebfb91ed60a07e5386f1987601)) + +## [2.4.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.7...v2.4.8) (2024-09-30) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.18 ([5f415ab](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5f415ab76f35909458092bf2f41b44e1714997bb)) + +## [2.4.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.6...v2.4.7) (2024-09-25) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.14 ([74d107a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/74d107a633859e3f3f261dc417c4a01eaba4e8af)) + +## [2.4.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.5...v2.4.6) (2024-09-17) + + +### Bug Fixes + +* **tests:** fix x509 tests ([381da8c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/381da8c93b15583eb95fe352f1d2f5cdc6e6eff4)) +* **x509:** fix x509 authentication ([b213f81](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b213f81beac6b3f624ae9a9564e8b92904a06c9c)) + +## [2.4.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.4...v2.4.5) (2024-08-29) + + +### Bug Fixes + +* **requirements:** update metaflow dependency ([1e658c4](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1e658c4a0b4857ca211462033900a592351ef6fe)) + +## [2.4.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.3...v2.4.4) (2024-08-20) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.7 ([588c4b0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/588c4b0607b94c656f47a0832fbcf513190d19c1)) + +## [2.4.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.2...v2.4.3) (2024-08-19) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.6 ([f5ac414](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f5ac4141ce29a150fe7fcbbf5ffeb48214944cf0)) + +## [2.4.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.1...v2.4.2) (2024-08-05) + + +### Bug Fixes + +* **requirements:** update dependency ([8bb3182](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8bb3182fdfe0e3f1575888685151e45d6b77db2f)) + +## [2.4.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.4.0...v2.4.1) (2024-08-05) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.11.2 ([60bf3b5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/60bf3b581c358dd9bb5856c3073d69fead3e7072)) + +# [2.4.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.12...v2.4.0) (2024-07-30) + + +### Bug Fixes + +* **blackduck:** enable diagnostics ([3182609](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/31826092b96f2e4cf14a753467f46eef80136a58)) +* **blackduck:** enable signature scan for blackduck ([aa76a89](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/aa76a89e0bcbf1ab6a8467235bd656cb90a5667c)) +* **blackduck:** finalize blackduck signature scan ([abfb2ef](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/abfb2efd5df89a9e2d3acbea65e32c6268d65e33)) +* **blackduck:** remove path ([583114b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/583114b929e91010b7d6d3988c358064b6c7a662)) +* **blackduck:** set java home var ([1adb6df](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1adb6dfb4bbf179668b84b70be34452267796d74)) +* **blackduck:** trigger build with diagnostics ([6431f96](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6431f960743f3310ab14d87050247c3d0ec1362f)) +* **blackduck:** try and create diagnostics file ([3be3a67](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3be3a67d4b682ce2ef08c3b9866084476e48853f)) + + +### Features + +* **ai-api-client-sdk:** Adds new model resources to ai-core-sdk ([242444d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/242444df2acd496dba32ebdc20c184100fd712e1)) + +## [2.3.12](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.11...v2.3.12) (2024-07-15) + + +### Bug Fixes + +* **deps:** update public.int.repositories.cloud.sap/suse/sle15 docker tag to v15.6.47.5.15 ([90a392d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/90a392d9c96020fbb8830de365c0489edf22814e)) + +## [2.3.11](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.10...v2.3.11) (2024-07-04) + + +### Bug Fixes + +* **dependencies:** update dependency on client sdk ([206af87](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/206af8727d9b99859eb102dcba3cc1392a358571)) + +## [2.3.10](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.9...v2.3.10) (2024-06-25) + + +### Bug Fixes + +* **renovate:** improves renovate config ([9bad228](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9bad2288b1ad44a675ed4991b2e6361945de6104)) + +## [2.3.9](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.8...v2.3.9) (2024-06-24) + + +### Bug Fixes + +* **template:** use sap-internal service plan ([8f67709](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8f67709e56db702cc07748c9933c25c5e0105b6b)) + +## [2.3.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.7...v2.3.8) (2024-06-20) + + +### Bug Fixes + +* **configure:** fix auth url for certificate ([35abeb7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/35abeb78b7874109b8fd48bede0764aa10d9f72d)) +* **configure:** fix auth_url value ([141654c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/141654cf7bead42efba6094330bb47b7b6bac287)) + +## [2.3.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.6...v2.3.7) (2024-06-20) + + +### Bug Fixes + +* **blackduck:** file upload for blackduck ([b510e82](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b510e82d5252a88d8d5d8749e54eac560f82caa4)) +* **credentials:** fix values read from env ([db993b3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/db993b345f4a2d2ee7858d9f511ef3770aa419c4)) + +## [2.3.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.5...v2.3.6) (2024-06-12) + + +### Bug Fixes + +* **applications:** cleanup applications after test ([dc207ce](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dc207ce895433cab5a6723484a81e96149d84c1a)) + +## [2.3.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.4...v2.3.5) (2024-06-12) + + +### Bug Fixes + +* **blackduck:** disable blackduck signature scan ([45ddb17](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/45ddb1739219e55c2aec3ac8fe5453251a4a3c47)) +* **blackduck:** fix docker image ([470aaad](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/470aaade68b50e7afc86732482d53633e3a1d56d)) +* **blackduck:** revert blackduck docker changes ([721acb3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/721acb3f08847bc669e610ae25229f4dd35239f8)) +* **setup:** fix pgk-info and add new client sdk version ([4031f88](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4031f88162563bb1d5353c27b83e2a9442c62254)) + +## [2.3.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.3...v2.3.4) (2024-06-05) + + +### Bug Fixes + +* **requirements:** update dependency to ai api client sdk ([3b9fdbc](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3b9fdbceae0003009e9f7aad36da93b68d6df405)) + +## [2.3.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.2...v2.3.3) (2024-06-04) + + +### Bug Fixes + +* **blackduck:** persist blackduck results to cumulus ([2179c65](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/2179c6564f8167d261d2f8df01717ba47c90dad4)) + +## [2.3.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.1...v2.3.2) (2024-06-03) + + +### Bug Fixes + +* **blackduck:** exclude test requirements ([ef8cef7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ef8cef7803ca334897042e094f2bd8c50aadd0d4)) +* **blackduck:** remove pylint installation ([8e7df0c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8e7df0c43eb33e6ec1c47716dac4d165a74e0941)) +* **config:** update group name ([a413fcd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a413fcd61352737a5c339ebf43f0de530d7fdb8f)) + +## [2.3.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.3.0...v2.3.1) (2024-06-03) + + +### Bug Fixes + +* **ci:** switch to correct black duck group ([ff9cadd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ff9cadd662e9e075acd25f03ae98b783d23a0c95)) + +# [2.3.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.2.0...v2.3.0) (2024-05-24) + + +### Bug Fixes + +* **pylint:** address pylint issues ([6bc2ddb](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6bc2ddb85793c3f2371f6379f940f96efa6e227c)) +* **requirements:** update ai api client SDK dependency ([78f300e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/78f300e5bd2fbc1b46a488de21f00b614d131646)) +* **sonar:** address sonar issue ([adc364e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/adc364e59f44864ff1f9fae34557dbd6b6f1d37e)) +* **x509:** fix issue and adjust tests ([58e21b8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/58e21b8d1b044fcacc28e65bb7a2a5014d36fad7)) +* properly handles authentication when used within aicore ([50ca89a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/50ca89a838ba21ad65c8a93691f8228e85272406)) + + +### Features + +* **x509:** add support for auth with X.509 certs ([85edc77](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/85edc77d337545e236c43125398fe4b98474720a)) + +# [2.2.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.1.3...v2.2.0) (2024-05-21) + + +### Bug Fixes + +* **pylint:** address pylint issues ([87d2d18](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/87d2d1812f6218846a28e7abbe080f84e77a9ce4)) +* **sonar:** address sonar issue ([e53863c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e53863c765a85a2ece4ba4f865fa9584d298b35b)) +* **x509:** fix issue and adjust tests ([14fc501](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/14fc50192dd623790ebd007ddd72437b27ffc530)) + + +### Features + +* **x509:** add support for auth with X.509 certs ([813eeab](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/813eeab27d42185c7bafb430710c5a8040932427)) + +## [2.1.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.1.2...v2.1.3) (2024-05-13) + + +### Bug Fixes + +* properly handles authentication when used within aicore ([546803f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/546803fea81b8b730d8ef33c9acfc278d9752f85)) + +## [2.1.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.1.1...v2.1.2) (2024-04-10) + + +### Bug Fixes + +* **deprecated endpoint:** adjust to new property ([a2b82bb](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a2b82bbd3137618867a9d04c19deae05bebb57d2)) +* **requirements:** update ai api client SDK dependency ([8377a1e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8377a1ea2c98423892e96f7c4c5d3c681ee6cec4)) + +## [2.1.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.1.0...v2.1.1) (2024-02-01) + + +### Bug Fixes + +* **cli:** create config dir if it not exists ([c751946](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c751946d4b1b0b07a40cbb174fb124f74d8293ec)) + +# [2.1.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.5...v2.1.0) (2024-01-29) + + +### Features + +* **credentials:** add support to pull credentials from VCAP (CF Service Bindings) ([4c11c2f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4c11c2fcee0269648289de6171251c3ef40501da)) +* **credentials:** add support to pull credentials from VCAP (CF Service Bindings) ([93cd6ed](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/93cd6ed5cceb50f19b592b5442ee0c5985c527e7)) + +## [2.0.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.4...v2.0.5) (2024-01-25) + + +### Bug Fixes + +* **requirements:** update client sdk version ([228cfff](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/228cfff0a9f9919b8a6190bee6f2fe0214f7afb1)) + +## [2.0.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.3...v2.0.4) (2024-01-12) + + +### Bug Fixes + +* **dummy:** trigger release ([#151](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/151)) ([e95aef7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e95aef72ccfefae24358a96cf5a06fa5dfcff961)) + +## [2.0.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.2...v2.0.3) (2024-01-10) + + +### Bug Fixes + +* **checkmarx:** set correct scan preset ([8f5c8ad](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8f5c8ad99bdf5b2de267fae226e57876cd84557b)) + +## [2.0.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.1...v2.0.2) (2023-12-13) + + +### Bug Fixes + +* **versions:** adjust versions and remove outdated dockerfiles ([f49d29e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f49d29e0a263c1f263ee663bd811e4094376d6e2)) + +## [2.0.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v2.0.0...v2.0.1) (2023-12-07) + + +### Bug Fixes + +* **nose:** remove nose dependency ([52e00f7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/52e00f7fdc92e32a24a31f6a908643c45d062762)) + +# [2.0.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.23.1...v2.0.0) (2023-12-05) + + +### Bug Fixes + +* update requirements.txt ([5921aa9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5921aa94735e90cc2ee83a40f18e3d334610b540)) + + +### deprec + +* **error-handling:** Deprecate AIAPIRequestException ([0a79125](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0a79125c4bf3cf0b6b2cfb60060a05844ce281df)) + + +### BREAKING CHANGES + +* **error-handling:** The AIAPIRequestException has been removed. Instead of this exception, the more specific, originally raised exception is being thrown. + +## [1.23.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.23.0...v1.23.1) (2023-12-04) + + +### Bug Fixes + +* **ai-api-client:** update dependencies ([5845a56](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5845a56de1a055e0f56109fba58c75205c8e8267)) + +# [1.23.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.22.4...v1.23.0) (2023-11-21) + + +### Features + +* **token:** Enable token caching ([b50d743](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b50d74376ebbb5f449d1e07a765d8582223bee6b)) + +## [1.22.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.22.3...v1.22.4) (2023-07-27) + + +### Bug Fixes + +* **requirements:** Fix old pyyaml version breaks installation ([#137](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/137)) ([a5d39cc](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a5d39cc33a2bbb8f54bbdf8b630e15d409521655)) +* **requirements:** fix prev commit message ([#138](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/138)) ([59ef3e8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/59ef3e89f5f25e5f371f2cd33332bcc4eba2a4c5)) + +## [1.22.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.22.2...v1.22.3) (2023-06-01) + + +### Bug Fixes + +* use latest ai-api-client-sdk (1.28.0) ([f4f3d25](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f4f3d25df32328a232fce205002dc79412a32fc5)) + +## [1.22.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.22.1...v1.22.2) (2023-05-25) + + +### Bug Fixes + +* **scripts:** change script permission ([1dffd70](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1dffd70e584311064eb4c6a37c6a050c224be49f)) + +## [1.22.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.22.0...v1.22.1) (2023-05-25) + + +### Bug Fixes + +* **tests:** adjust for worker cluster setup ([3077bfa](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3077bfa9ca2d8eb69647a68c457b6efc5e908669)) + +# [1.22.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.21.0...v1.22.0) (2023-05-10) + + +### Features + +* **client:** udpate client sdk dependency for release ([5c65bec](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5c65bec58bdc08ebf62f291b5e08a40ed5af0f5a)) + +# [1.21.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.20.0...v1.21.0) (2023-05-02) + + +### Features + +* **embedded:** add documentation ([#127](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/127)) ([6ca8323](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6ca832354999a855f214daaaf236f9ee6081fc94)) + +# [1.20.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.19.2...v1.20.0) (2023-04-27) + + +### Features + +* **embedded:** add support for embedded code packages ([#126](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/126)) ([f0c6333](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f0c6333631daa23411e498a4434e2cf075a37ee0)) + +## [1.19.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.19.1...v1.19.2) (2023-03-21) + + +### Bug Fixes + +* **release:** trigger release ([8e6d880](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8e6d880bc6da6f32774c1b13db9f4430208009b5)) + +## [1.19.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.19.0...v1.19.1) (2023-03-20) + + +### Bug Fixes + +* **requirements:** use ai-api-client-sdk 1.26.2 ([09e13a0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/09e13a0f6f48ac5887094ae07b58c5bb4f753d53)) + +# [1.19.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.18.3...v1.19.0) (2023-02-09) + + +### Features + +* **secrets-endpoint:** Add admin/secrets endpoint capability ([c04caa9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c04caa9471297df1062e36dd0c53cda76c59fa03)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([07d5e5b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/07d5e5b82cdf3b877df67d2680e1c7b8fca42d9c)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([6a03dc2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6a03dc2260f93ee5cbb16fc16b2c3b0ec6f2a95e)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([de3e4ce](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/de3e4ce341bd898becc6987867d55a1fd4b3d923)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([20a0c95](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/20a0c95564be6e7456e2a92d778c17b7756f6992)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([e91a2df](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e91a2dfb4e6e0aa9e854676565cf79adf568e0ae)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([fc8dfc8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fc8dfc8f9d1b328fbbc25a699616ade2153f6251)) +* **secrets-endpoint:** Add admin/secrets endpoint capability ([bc950dd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/bc950dd2273867abb9522cbc838a3151a3b74572)) +* **secrets-endpoint:** Implement feedback ([e9cb047](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e9cb047979b4c386e6b6c998593306122afae17e)) + +## [1.18.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.18.2...v1.18.3) (2023-01-19) + + +### Bug Fixes + +* **requirements:** upgrade ai-api-client-sdk version ([8f350b9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8f350b97df8e5aade40104fa6f0f3f46c3645476)) + +## [1.18.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.18.1...v1.18.2) (2022-11-29) + + +### Bug Fixes + +* **clientsdk:** upgrade client sdk to 1.24.1 ([22da37e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/22da37ecb0480d8ce5c5d62c3b4ab00c69604f13)) + +## [1.18.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.18.0...v1.18.1) (2022-11-22) + + +### Bug Fixes + +* **content:** fix scan complains ([d1c23e3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d1c23e30882c883a09ae12f57b8e6b0bc005c2d3)) +* **content:** fix scan complains ([779ba50](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/779ba507f1a7790b40109fecfdf8f07df5f84390)) + +# [1.18.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.17.1...v1.18.0) (2022-11-10) + + +### Features + +* **content:** Add deployment templates generation ([#112](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/112)) ([4823f9b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4823f9b060be40555c18f30bfaab0bddd0278407)) +* **content:** fix commit message ([e05ae70](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e05ae703c701708975c7750b83a2290fd73b1536)) + +## [1.17.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.17.0...v1.17.1) (2022-09-22) + + +### Bug Fixes + +* **models:** fix from_dict for some models ([b7e7691](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b7e769133d849c95aa0da593951382273fe91af3)) +* **requirements:** update ai-api-client-sdk ([3dcffcd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3dcffcd868f25095915c27993e6fc63efce53a2f)) +* **requirements:** update ai-api-client-sdk ([7e30dab](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7e30dabd7e080872e8b0e1543fceb0c79a9b573e)) + +# [1.17.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.16.2...v1.17.0) (2022-09-05) + + +### Features + +* **requirements:** Bump ai-api-client-sdk to 1.23.0 and replace resourcegroup implementation by functionality implemented in ai-api-client-sdk ([44d0f38](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/44d0f3823e49211a334123e9e18422685dea7d57)) +* **requirements:** Bump ai-api-client-sdk to 1.23.0 and replace resourcegroup implementation by functionality implemented in ai-api-client-sdk ([f66b023](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f66b02361fc65a7610b5b7fbe48bdf4d6e7dfd94)) + +## [1.16.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.16.1...v1.16.2) (2022-08-17) + + +### Bug Fixes + +* **cumulus:** correcting the pipeline mappings for cumulus ([4b40d04](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4b40d04dad411899660762bce5f273b895e73375)) + +## [1.16.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.16.0...v1.16.1) (2022-08-15) + + +### Bug Fixes + +* **requirements:** Bump ai-api-client-sdk to 1.22.0 ([ab1de46](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ab1de464f669906450bf0db66284cc35b88e3a62)) + +# [1.16.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.15.1...v1.16.0) (2022-08-03) + + +### Features + +* **meta:** include meta client ([56359ba](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/56359ba62f10494d2e3ae8a3825f276d9535a5c0)) + +## [1.15.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.15.0...v1.15.1) (2022-08-01) + + +### Bug Fixes + +* bump ai api client sdk to 1.20.0 ([0f08d91](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0f08d91a12110374d00ab8ab709735da1b8e1d8f)) + +# [1.15.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.14.3...v1.15.0) (2022-07-28) + + +### Bug Fixes + +* added code samples ([fc280ea](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fc280ea4478022e3533ba34a7166a3c26fa2006d)) +* added model imports ([a8c46d1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a8c46d1e9726d5a8aff2464588c7ed1ba31eaf9b)) +* fixed code smell ([c83cf7d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c83cf7deaeedfdf3696ab5879483431de1fdb125)) +* fixed test case ([c4c49cf](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c4c49cf351604083c3cf07787617981ce46144cd)) +* fixed tracking module bug ([0de0ae8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0de0ae88aa090f01ec4d8c998fabaf6cd5d1f61d)) +* updated acceptance test case ([5dcd60b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5dcd60b558b382daef65214c6b0e26352e9103e0)) +* updated integration test cases ([f311eee](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f311eeef58345dcac8506ddd0466addbd99daf68)) +* updated readme ([0b0287c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0b0287c4a6c61f9ea9f0588fbbaeea27fad495a8)) +* updated readme as per comments ([72bebe9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/72bebe9fbe837beebbc21871eb74e57f56f87a3c)) +* updated wordings ([d15fe0a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d15fe0ad410d2d16565d4d7ca04532dc0f4df61e)) + + +### Features + +* **objects:** Better string representation of objects ([22c52da](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/22c52da9ec019a5fe9b05bfd3f29007f9e06798a)) +* **objects:** Implemented feedback for better string representation of objects ([c486101](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c4861013bf5f371bee3ca23df96aa8741d04b06e)) + +## [1.14.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.14.2...v1.14.3) (2022-07-19) + + +### Bug Fixes + +* **requirements:** bump ai-api-client-sdk to 1.19.1 ([26e3de9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/26e3de99b8b04be9ee484be4558bccc6f814333e)) + +## [1.14.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.14.1...v1.14.2) (2022-07-14) + + +### Bug Fixes + +* modified metricscoreclient to pass in local experimentation ([#92](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/92)) ([d165bf5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d165bf57efc5dc54d944cb9e50511f4e544ff4d2)) + +## [1.14.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.14.0...v1.14.1) (2022-07-06) + + +### Bug Fixes + +* **checkmarx:** add checkmarx report to cumulus ([cb66775](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/cb66775ff305becb3d1cdc97cc7670ed5bbc15bf)) + +# [1.14.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.8...v1.14.0) (2022-07-05) + + +### Features + +* **restclient:** Added timeout and retries ([da3e185](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/da3e1850bb2eed439e7f077549f8ea738d660e44)) +* **restclient:** Fix Unittests (pyhumps version) ([4fb5fd7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4fb5fd7f3d8f80256bf3b071b5d1d4a21625f2ee)) +* **restclient:** Ignoring constants file in sonar ([c76d510](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c76d51079f782f7d326350ba2d001417731a6a47)) +* **restclient:** Implemented feedback ([b2984bd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b2984bdfd8ee552441d69bb071721fe54b37602c)) +* **restclient:** Implemented feedback ([dd8cb66](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dd8cb667dc00cb16d2a1263655785888830171a7)) + +## [1.13.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.7...v1.13.8) (2022-06-30) + + +### Bug Fixes + +* **ai-api-client-sdk:** update ai-api-client-sdk ([3a255ed](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/3a255ed815d2acb992ae528c839b37c3deec3c72)) +* **pylint:** fix pylint ([83d6d70](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/83d6d7061bacb828bfaec28f102553cb90c57092)) +* **pylint:** fix pylint issue ([40de424](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/40de4245e84f08f901de776ac5912ea5736bb5cd)) +* doucmentation in ApplicationSource ([ef66882](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ef66882225dbdb05ad73a8e5bb33db055d6a6236)) + +## [1.13.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.6...v1.13.7) (2022-06-29) + + +### Bug Fixes + +* **cumulus:** add whitesource to cumulus upload ([388f624](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/388f6249c8146783c092bc513cdb82d3850f7543)) +* **whitesource:** add missing whitesource scan ([181c075](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/181c07565c9366355b46e124572dca7e93815e9d)) + +## [1.13.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.5...v1.13.6) (2022-06-29) + + +### Bug Fixes + +* **dummy:** trigger release build ([9a76a0c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9a76a0c836bfdc2a89d743fc9bf715376cf65cfc)) + +## [1.13.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.4...v1.13.5) (2022-06-28) + + +### Bug Fixes + +* **cumulus:** extend cumulus config as ai api facade ([6a9fb22](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6a9fb22393fd8a8e29fe38324cbc2f4722b1d3e5)) +* **cumulus:** extend cumulus config as ai api facade ([5b12750](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5b127505f4f6097a54e6606073a4eb34c7096af2)) +* add protecode scans to cumulus ([1917a25](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1917a250ce700c49d5e130c304f93e8ba3162eaf)) + +## [1.13.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.3...v1.13.4) (2022-06-27) + + +### Bug Fixes + +* **docu:** Adjust to Markdown standards ([#78](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/78)) ([9e165ee](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9e165ee56378f7e3e3d99e4dbf6ec704730fc67e)) +* **docu:** fix additional issues ([#80](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/80)) ([e9099b7](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e9099b741431c9e8877e670c49cd8ea470e2612e)) + +## [1.13.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.2...v1.13.3) (2022-06-15) + + +### Bug Fixes + +* **lint:** install older pylint version ([9f302d3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9f302d3e80d0ba5b35c308066d530cca0fefb6bf)) +* **pylint:** fix the version ([73ec9d1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/73ec9d1a631c7a9d0bbac8b2bc2e4e007f8c2579)) +* update requirement ([dce4df8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dce4df8de06656f49a257e35f437455e613e8e6e)) + +## [1.13.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.1...v1.13.2) (2022-05-19) + + +### Bug Fixes + +* **doc:** install missing dependency ([#73](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/73)) ([63c90fb](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/63c90fb32c4d10290de08b0126d06a77501a12fc)) + +## [1.13.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.13.0...v1.13.1) (2022-05-17) + + +### Bug Fixes + +* **docu:** Correct pypi url for sap.com ([b8b3a6a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b8b3a6accc40f3d2bb8da19b9d65b053a427f7f4)) + +# [1.13.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.12.2...v1.13.0) (2022-05-16) + + +### Features + +* **content:** add content cli ([f637ea0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f637ea0ea792af3feb9211391a9f80e61bd0eac1)) + +## [1.12.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.12.1...v1.12.2) (2022-05-11) + + +### Bug Fixes + +* **documentation:** Shift the documentation to pypi ([b4727cd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b4727cd5f1fca67b1719016826cfb9a2b13cb417)) + +## [1.12.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.12.0...v1.12.1) (2022-05-04) + + +### Bug Fixes + +* **requirements:** update ai-api-client-sdk ([7ed4b0b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7ed4b0b8223391101814795414b14de0ab9b4ef9)) + +# [1.12.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.11.0...v1.12.0) (2022-04-21) + + +### Bug Fixes + +* added acceptance test ([e23cab4](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e23cab49ef43228ed4e0ab6d7a823e6896169b41)) +* added changes ([68a735e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/68a735e7fa5b6fd2fd265827762447842ffaebe2)) +* added files ([99d74b5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/99d74b52761ce1057c038f34dfe7a984369f0278)) +* added gitops pr validate script ([585eb04](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/585eb0404c7cb58b599da70cb991b11f9771e148)) +* cleaned up test requirements.txt ([d2213b1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d2213b1029fbf887eb27eddf928f83b7b28f35da)) +* cleanup and updated versions ([e71bf81](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e71bf81700cf8ec2e7c84fed2ed33e34412bbd20)) +* code smells ([5d0d8d6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5d0d8d6d5eadad101a361e5c22f38aedb5a7f743)) +* fixed code conflicts ([1cc158d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1cc158d3a79e8e6c5c302ebcce99d733ab0ac7cc)) +* fixed import issue ([b5de702](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b5de7029d3ee99f6577a1178c9d5032ae39d8a34)) +* fixed integration test ([fa3592e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fa3592e09d2bfacde2f86cf69993ed8a917dbd79)) +* fixed integration test cases ([04b038a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/04b038a67da21e507e3815589d3b7911a23176a0)) +* fixed merge conflicts ([f2175a8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f2175a8930252057510871359057c320faf0c915)) +* modified template ([e2acb65](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e2acb65a01f2ca7b197433fb36d193dcce4ec810)) +* refactored code ([288845b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/288845b4032dc199f6868abd67b85930b06ca836)) +* removed unnecessary changes ([9e23f85](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9e23f85274d7a152a24dde7a9c57136d35547214)) +* resolved merge conflicts ([f6cd3c3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f6cd3c3a5342c81d37bcd9842326c7b1baf4d69f)) +* reverted typo ([691f5be](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/691f5be2a996a7b75ebe49d7621471a92f468d8e)) +* updated and added files ([c2ffe75](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c2ffe75f2016e097eb8c1e3de65719b9dd70fbad)) +* updated client sdk version ([bfb6c32](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/bfb6c3209e9a437d2d4e66621513343c84b542df)) +* updated config files ([fe84327](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fe843272797cfe4ff0847d1d60ce4ed04b615621)) +* updated folder name pr validate ([1e80158](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1e801588056bc01e71b10c327efe76c523cef195)) +* updated integration test cases ([b7f9ab2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b7f9ab2d1de796d1cc49e7bcccea69bf1c58b78c)) +* updated version ([872249f](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/872249f23d5a6678e040d6704cbdd05855344343)) + + +### Features + +* enabling usage of tracking module of ai-core-sdk within aicore cluster ([80a727e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/80a727e778d6cac343a4a72a8fe9420f40b806f2)) + +# [1.11.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.6...v1.11.0) (2022-04-21) + + +### Features + +* enabled usage of tracking module within aicore cluster ([#67](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/67)) ([b717547](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b717547fd43c6105fa87e9d66518f1b0bf6abc92)) + +## [1.10.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.5...v1.10.6) (2022-04-13) + + +### Bug Fixes + +* **test:** fix application e2e tests ([d733b08](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d733b087093a2dde66c22331fcf7a56aeb46b9a6)) +* **tests:** fix applications e2e test ([84682f1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/84682f1702518c38a69228397a1267dd6af08629)) +* **tests:** fix applications tests ([9d7940c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9d7940c86a52c1a053198ea7c5e4277067d109e0)) + +## [1.10.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.4...v1.10.5) (2022-03-23) + + +### Bug Fixes + +* dummy commit ([d5f1555](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d5f15558f62c426a72ffbe55e1aace9b5e4befb5)) +* fixed artifact name bug ([9cea558](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9cea55863ada716e33f90802c813d34662d706e5)) +* fixed unit test case ([0f3c730](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0f3c730fe3585885897af464bdb731a904ca8ad3)) +* resolved merge conflicts ([a329d32](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/a329d32ea5ff28243e2e26ee19a43e7e96fb7d65)) +* reverted changes ([eeb0218](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/eeb0218f21c8bbb4ed02641eaf791ffea6c24d56)) +* reverted tracking enablement changes ([79a4a36](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/79a4a36f02c66a526c081ac39642e3bbca846cca)) + +## [1.10.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.3...v1.10.4) (2022-03-23) + + +### Bug Fixes + +* **pylint:** fix pylint issue ([70e1f47](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/70e1f47ac4334cb9779568072524e75917cfddcb)) + +## [1.10.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.2...v1.10.3) (2022-03-22) + + +### Bug Fixes + +* **pylint:** fix pylint issues ([4ab578c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4ab578c858f565bf7b9faef9409220da9baa1129)) + +## [1.10.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.1...v1.10.2) (2022-03-22) + + +### Bug Fixes + +* **tests:** content and templates ([dbc0452](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dbc04525173cc9592f7f7a321fefb0e0d593a2f9)) +* **tests:** content and templates ([4af2774](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4af27743c7db01b141adce6525265c05a6ac590d)) + +## [1.10.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.10.0...v1.10.1) (2022-03-17) + + +### Bug Fixes + +* pypi description ([895c911](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/895c9116c350455a1a054ab1a1346bcd64563d6a)) + +# [1.10.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.9.1...v1.10.0) (2022-03-17) + + +### Bug Fixes + +* **versionfortest:** add acceptance tests version update ([f7596e9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f7596e984af3f00ba21adca9d424a766a39af65c)) + + +### Features + +* **requirements:** update client sdk version ([50547af](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/50547af7fee5123ecef2aea3c34684f8c3a48eac)) + +## [1.9.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.9.0...v1.9.1) (2022-03-08) + + +### Bug Fixes + +* added acceptance test cases ([#51](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/51)) ([1e93c8b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1e93c8b0f996161f731199a1eedfc984204b6c73)) + +# [1.9.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.8.0...v1.9.0) (2022-03-01) + + +### Features + +* **requirements:** update ai-api-client-sdk ([c3e3f6e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c3e3f6e16e65bcefee2ce4d777d83abe1a268ff7)) + +# [1.8.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.7.1...v1.8.0) (2022-03-01) + + +### Bug Fixes + +* **requirements:** revert change ([0de462c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/0de462cefe6014c0a76f2ff5cce5ddc4e5f97443)) + + +### Features + +* **deployment:** patch deployment with conf id ([627a11b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/627a11ba55b549ca9e41838165be9481cb12e1b1)) + +## [1.7.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.7.0...v1.7.1) (2022-02-21) + + +### Bug Fixes + +* **req:** Update requirements.txt ([b2442b6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b2442b69061bc2cb8495508889a02d82ed1792a1)) +* **tests:** formatting as requested ([73a7e85](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/73a7e85b1312d6e923a82a97722817583f4e5441)) +* **tests:** try to fix metrics tests ([c7ae4c0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c7ae4c03c53ad702909c496b273f1161ac3b37be)) +* **tests:** try to fix metrics tests 2 ([d746cd0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d746cd05218b9c274b228518f038b9c2cfc63a5b)) + +# [1.7.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.6.1...v1.7.0) (2022-02-16) + + +### Features + +* **aicoreclient:** enable metrics client in cluster ([bf5aff8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/bf5aff8af3bff21798a8f3b16d191d23cefe8936)) +* Enabling usage of metrics module within cluster and convenience function suppport ([#45](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/45)) ([365723c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/365723c2ce20d5d2ad4d24e683227277dfec8493)) + +## [1.6.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.6.0...v1.6.1) (2022-02-15) + + +### Bug Fixes + +* **models:** accept additional args in model inits ([6388a4a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/6388a4a21b3a03d1a19e338ce4de0a897df68a64)) + +# [1.6.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.14...v1.6.0) (2022-02-01) + + +### Bug Fixes + +* **sdk:** make api description more clear ([209e3a3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/209e3a35de4b5dfa18a252a9f606693bae536fbc)) +* **sdk:** resolving PR comments ([f8bdf57](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f8bdf576a6616307f939a110c1ceda7d232bed75)) +* **sdk:** resolving PR comments further ([2f2bdef](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/2f2bdefe6a93811a0cb23c8f217fd758f3e248be)) +* **sdk:** resolving PR comments further 3 ([31dfc4d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/31dfc4dd6fcded769390de0d5884cac86131bc6d)) +* **sdk:** update ai api client requirement ([5cfed41](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/5cfed416634aa73fa61c9772c41b883ce9a4b6c8)) + + +### Features + +* **sdk:** use newer ai api client sdk ([712aee4](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/712aee42eeccd949ffca1367509322477c804a04)) + +## [1.5.14](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.13...v1.5.14) (2022-02-01) + + +### Bug Fixes + +* bump ai-api-client-sdk req to version 1.14.1 ([198964d](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/198964d706ed1b631226610dd4d9e441161af25d)) + +## [1.5.13](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.12...v1.5.13) (2021-12-21) + + +### Bug Fixes + +* add python 3.9 support ([caf7941](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/caf794147e9683af7a6e7542adce413d68389bd7)) +* bump ai-api-client-sdk requirement ([393097c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/393097c072a40cac0dc6656c17cc41396c7c9497)) + +## [1.5.12](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.11...v1.5.12) (2021-12-13) + + +### Bug Fixes + +* bump ai-api-client-sdk requirement ([253b295](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/253b2959db8bf8d49520223edfc3e0663896ce66)) +* ignore tests in wheel ([06dc87b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/06dc87b9293f04c6499e97668d6ad3724fd33398)) + +## [1.5.11](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.10...v1.5.11) (2021-12-06) + + +### Bug Fixes + +* **requirement:** add new requirement and add python 3.6 due to our old pipelines ([b5b2510](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b5b251061e6e40abc3eec26aed74dcd91a9e4d0d)) +* **tests:** update pip to 21.3.1 ([d880781](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d88078192450dfa7d3c260e745d58eab1dae78ed)) +* **tests:** update to the new docker repo ([b594e72](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b594e72f22f6769af322a1f5282ed65f80364035)) +* Bump ai-api-client-sdk version in requirements ([32d7912](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/32d791270ce00d7953d563f9904a9d6b66982d17)) +* further experimenting ([53a3704](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/53a3704b53c5192ea565fdd688b8070c0bf3f277)) +* further experimenting 2 ([65f0f61](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/65f0f6112ee62503668ffaae906f0603acee5b16)) +* make requirements more dynamic for the future ai-api-client PyPI release ([292194c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/292194c014edbf0400c2b9c68094d5cee3ded8be)) +* try to use a new pip ([451be5c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/451be5cb852c0bea55c202fe34682c4bcd63bf0e)) +* try to use a new pip 2 ([d9385ad](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d9385ad61159149618cb0888a88a01b5589b0f45)) +* try to use a new pip 3 ([37deb75](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/37deb758d7cc9bf6f28dc7cfdb24e4f424be9db4)) +* use a newer build image (inspiration from https://github.wdf.sap.corp/ICN-ML/aicore/ ) ([137480a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/137480a50d2e063a8249799fd8ff49fa2b417a3a)) + +## [1.5.10](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.9...v1.5.10) (2021-11-26) + + +### Bug Fixes + +* Fix error messages reported here https://github.wdf.sap.corp/PyPI/SAP_AI_CORE_SDK-1.0/pull/1 ([4c8e9e9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4c8e9e9fa154e6ce06d51ce046f0bca2699f6353)) + +## [1.5.9](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.8...v1.5.9) (2021-11-24) + + +### Bug Fixes + +* Fix description an dkeyword in METADATA ([ab090bd](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ab090bd05a0a373cf77adc097961b3a1e4fc964e)) + +## [1.5.8](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.7...v1.5.8) (2021-11-22) + + +### Bug Fixes + +* **docs:** add nexus to index for docs generation ([c45b7ef](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c45b7ef96c6c6ea777485b6a5f35f91c162da8e1)) + +## [1.5.7](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.6...v1.5.7) (2021-11-22) + + +### Bug Fixes + +* **docs:** fix docs generation ([6583396](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/658339664e7a7e87feca589b4ad16f0d91648ad2)) + +## [1.5.6](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.5...v1.5.6) (2021-11-10) + + +### Bug Fixes + +* **imports:** import ai-api-client-sdk exceptions ([c9fbc1a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c9fbc1ac4be8a0fc92aacd5e424fd8f5077ae0d4)) + +## [1.5.5](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.4...v1.5.5) (2021-11-05) + + +### Bug Fixes + +* **imports:** import modules from ai-api-client-sdk ([d9477c9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d9477c90c194bef9cebb0da4877f7c799e20efb5)) + +## [1.5.4](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.3...v1.5.4) (2021-11-04) + + +### Bug Fixes + +* **import:** fix import statements ([4f4cd21](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4f4cd210fa209b6e4ddd58cbf5cacdfe33b27d7d)) + +## [1.5.3](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.2...v1.5.3) (2021-11-04) + + +### Bug Fixes + +* **object_store_secrets:** add resource_group parameter ([ac144d8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ac144d808b814ecc04dafa512dad727ebd79d0b8)) + +## [1.5.2](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.1...v1.5.2) (2021-11-04) + + +### Bug Fixes + +* **explicit typing:** type all the properties explicitly ([1a9e202](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1a9e202d5c34ec9a02d9ac9e134c441a08344993)) + +## [1.5.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.5.0...v1.5.1) (2021-10-29) + + +### Bug Fixes + +* fix sonar issues ([8d9d39b](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/8d9d39b2ceb7d1d265de3f53a5180d8c641fe8ed)) +* fixed sonar issues ([dc2b2b8](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dc2b2b81823d88a90a8aff7b734138987f40defa)) + +# [1.5.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.4.0...v1.5.0) (2021-10-28) + + +### Bug Fixes + +* addressed review comments ([d99e99c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/d99e99c3ac53fc85c9335d846adff1da8acc2af2)) +* code cleanup ([fe0c56a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/fe0c56a386db20c3a83c45f57ab3b6058129b53e)) +* fixed intergration test cases ([f0cabf5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f0cabf5736a3e9eafe826c7a9af890f8259ac30d)) +* fixed the logical issue ([cfdbeb2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/cfdbeb2a664d307c55316c4312ef030b1cda8d2d)) + + +### Features + +* added kpi endpoint functionality ([ce948e3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ce948e33decc6b4c76dd8bdf68c6bfd4a1bc5680)) + +# [1.4.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.3.0...v1.4.0) (2021-10-28) + + +### Bug Fixes + +* **metrics:** changes based on review ([21d2ab6](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/21d2ab6ad0f808cc38c04c3addb9c00758973cc9)) +* **test:** add test for exception case ([08bb3cc](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/08bb3cc27b70254093521b51da186a6edb8442bd)) + + +### Features + +* **metrics:** implement metric client for patch ([9fc5140](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/9fc5140da7ad12283a92a7e248a29c066d3e6b67)) + +# [1.3.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.2.0...v1.3.0) (2021-10-27) + + +### Features + +* **repositories:** implemented repositories client ([4e0962a](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/4e0962a9b11184cf41813628b2884152cf9ec47e)) + +# [1.2.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.1.0...v1.2.0) (2021-10-27) + + +### Features + +* **applications:** implemented applications client ([b6545f5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b6545f5f4a3f912ec499b74705e873dfae2afc08)) +* **resource_groups:** fix resource groups implementation ([dc2a375](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dc2a3756560baf84197e28e3bd183f51acd3fd3b)) +* **resource_groups:** fix typo ([dda4baf](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/dda4bafd633d853d70dc51991dc46d4e20b208e8)) +* **resource_groups:** implemented resource_groups client ([c68b0f1](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c68b0f14f3bb744b125ac7f41fa44005924478cc)) + +# [1.1.0](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.0.1...v1.1.0) (2021-10-25) + + +### Bug Fixes + +* addressed review comments ([e08a157](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/e08a157f9d3b4b85e02918866f5124eb5a150495)) +* fixed logical issues ([b6675c4](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b6675c434ce66d2d5554486f4315102c3ee7e699)) +* fixed model ([7f1c9f2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7f1c9f2a72809fba5fa40b01dda060a89c348252)) +* fixed prefix issue ([807c476](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/807c47694b08935d8941450845688e6e233e64f5)) +* fixed unit test ([1774a3e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/1774a3e637b8707d8c7db4c8627c35f819c61352)) +* fixed unit test ([964640c](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/964640c809d3295ea0403333f8c7ec04883241bd)) +* improved intergration test case ([68de693](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/68de693a5fd35bf4211a537970cf741ce3013e08)) +* moved file and removed unnecessary import ([ad24c71](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ad24c7176419185e7d8c1b31e2fcdf53240a95a6)) +* removed unnecessary file ([b3d43c5](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/b3d43c5a244dfdec18efe931dd5ca5ef00a46a47)) +* renamed file ([c9e60d0](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c9e60d0c5459b827464663d05d53358f094fb49c)) +* updated document ([73601d3](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/73601d38c2f5a3888a409144c296b17d2a471df2)) + + +### Features + +* added object store sdk endpoint clients ([#10](https://github.wdf.sap.corp/AI/ai-core-sdk/issues/10)) ([f2849e9](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/f2849e9e2d33823e2bd08f89fb30e67f59591b72)) + +## [1.0.1](https://github.wdf.sap.corp/AI/ai-core-sdk/compare/v1.0.0...v1.0.1) (2021-10-19) + + +### Bug Fixes + +* **dockerregistrysecrets:** add e2e tests ([c5d0d1e](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/c5d0d1e25d074a0e08ecbc73447d5411fbeee897)) + +# 1.0.0 (2021-10-11) + + +### Bug Fixes + +* **build:** Add vault config ([7786bcb](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/7786bcb887c5cba567234831bd61c06c6bdd5778)) +* **build:** Fix vault ident ([ad708d2](https://github.wdf.sap.corp/AI/ai-core-sdk/commit/ad708d2cc2316ae9f55c747f7d74f6faf7be918c)) diff --git a/packages/core/docs/ai_core_sdk.ai_core_v2_client.html b/packages/core/docs/ai_core_sdk.ai_core_v2_client.html new file mode 100644 index 0000000..c67c3de --- /dev/null +++ b/packages/core/docs/ai_core_sdk.ai_core_v2_client.html @@ -0,0 +1,150 @@ + + + + +Python: module ai_core_sdk.ai_core_v2_client + + + + + +
 
ai_core_sdk.ai_core_v2_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/ai_core_v2_client.py
+

+

+ + + + + +
 
Modules
       
os
+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
AICoreV2Client +
+
+
+

+ + + + + + + +
 
class AICoreV2Client(builtins.object)
   AICoreV2Client(
+    base_url: str,
+    auth_url: str = None,
+    client_id: str = None,
+    client_secret: str = None,
+    cert_str: str = None,
+    key_str: str = None,
+    cert_file_path: str = None,
+    key_file_path: str = None,
+    token_creator: Callable[[], str] = None,
+    resource_group: str = None,
+    client_type: str = 'AI Core Python SDK',
+    **kwargs
+)

+The AICoreV2Client is the class implemented to interact with the AI Core endpoints. The user can use its
+attributes corresponding to the resources, for interacting with endpoints related to that resource. (i.e.,
+aicoreclient.scenario)

+:param base_url: Base URL of the AI Core. Should include the base path as well. (i.e., "<base_url>/lm/scenarios"
+    should work)
+:type base_url: str
+:param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to
+    None
+:type auth_url: str, optional
+:param client_id: client id to be used for authorization, defaults to None
+:type client_id: str, optional
+:param client_secret: client secret to be used for authorization, defaults to None
+:type client_secret: str, optional
+:param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None
+:type cert_str: str, optional
+:param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None
+:type key_str: str, optional
+:param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter,
+    defaults to None
+:type cert_file_path: str, optional
+:param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter,
+    defaults to None
+:type key_file_path: str, optional
+:param token_creator: the function which returns the Bearer token, when called. Either this, or
+    auth_url & client_id & client_secret should be specified, defaults to None
+:type token_creator: Callable[[], str], optional
+:param resource_group: The default resource group which will be used while sending the requests to the server. If
+    not set, the resource_group should be specified with every request to the server, defaults to None
+:type resource_group: str, optional
+:param client_type: The client type which will be sent in the User-Agent header of the requests, defaults to
+    "AI Core Python SDK"
+:type client_type: str, optional
+:param \**kwargs:
+    * *read_timeout* (int): Read timeout for requests in seconds, defaults to 60s
+    * *connect_timeout* (int): Connect timeout for requests in seconds, defaults to 60s
+    * *num_request_retries* (int): Number of retries for failing requests with http status code 429, 500, 502, 503 or 504, defaults to 10
 
 Methods defined here:
+
__init__( + self, + base_url: str, + auth_url: str = None, + client_id: str = None, + client_secret: str = None, + cert_str: str = None, + key_str: str = None, + cert_file_path: str = None, + key_file_path: str = None, + token_creator: Callable[[], str] = None, + resource_group: str = None, + client_type: str = 'AI Core Python SDK', + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_env(profile_name: str = None, **kwargs)
Alternative way to create an AICoreV2Client object.
+Parameters for base_url, auth_url, client_id, client_secret, x.509 credentials (either as file path or string)
+and resource_group can be passed as keyword or are pulled from environment variables.
+It is also possible to use a profile, which is a json file in the config directory. The profile name can be
+passed as keyword or is pulled from the environment variable AICORE_PROFILE. If no profile is specified,
+the default profile is used.
+A specific path to a config, that should be used, can be set via the environment variable AICORE_CONFIG.
+The hierarchy of precedence is:
+1. keyword argument
+2. environment variable
+3. configuration file
+4. value from VCAP_SERVICES environment variable, if exists

+:param profile_name: name of the profile to use, defaults to None. If None is passed, the profile is read from
+    the environment variable AICORE_PROFILE. If this is not set, the default profile is used.
+    The default profile is read from $AICORE_HOME/config.json.
+:type profile_name: optional, str
+**kwargs: check the parameters of the class constructor
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
logger = <Logger ai_core_sdk (INFO)>
+ +

+ + + + + +
 
Data
       Callable = typing.Callable
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.cli.html b/packages/core/docs/ai_core_sdk.cli.html new file mode 100644 index 0000000..d4fdfe0 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.cli.html @@ -0,0 +1,64 @@ + + + + +Python: module ai_core_sdk.cli + + + + + +
 
ai_core_sdk.cli
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/cli.py
+

+

+ + + + + +
 
Modules
       
click
+
json
+
pathlib
+

+ + + + + +
 
Functions
       
confirm_resource_group(resource_group: 'Optional[str]' = None)
+
create_config(**kwargs)
# Utility Functions
+
create_config_file( + config_path: 'pathlib.Path', + auth_url: 'str', + client_id: 'str', + client_secret: 'str', + cert_file_path: 'pathlib.Path', + key_file_path: 'pathlib.Path', + base_url: 'str', + resource_group: 'str' +)
+
get_auth_url(auth_url: 'Optional[str]' = None)
+
get_base_url(base_url: 'Optional[str]' = None)
+
get_profile_config_path(profile: 'str')
+
get_str_value(msg: 'str', value: 'Optional[str]' = None)
+
is_valid_url(url, path_forbidden=True)
+
load_service_key(service_key_json: 'str')
+
prompt_for_input(prompt_text, is_url=False, path_forbidden=True)
+

+ + + + + +
 
Data
       AI_CORE_PREFIX = 'AICORE'
+API_V2_SUFFIX = '/v2'
+CORE_CREDENTIAL_VALUES = [CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid'), transform_fn=None), CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret'), transform_fn=None), CredentialsValue(name='auth_url', vcap_key=('credentials', 'url'), transform_fn=<lambda>), CredentialsValue(name='base_url', vcap_key=('cre...rviceurls', 'AI_API_URL'), transform_fn=<lambda>), CredentialsValue(name='resource_group', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'), transform_fn=<lambda>), CredentialsValue(name='cert_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='key_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'), transform_fn=<lambda>), CredentialsValue(name='key_str', vcap_key=('credentials', 'key'), transform_fn=<lambda>)]
+DEFAULT_CONFIG = 'config.json'
+DEFAULT_PROFILE = 'default'
+DEFAULT_RESOURCE_GROUP = 'default'
+MAX_TRIES = 5
+OAUTH_TOKEN_SUFFIX = '/oauth/token'
+Optional = typing.Optional
+cli = <Group cli>
+configure = <Command configure>
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.credentials.html b/packages/core/docs/ai_core_sdk.credentials.html new file mode 100644 index 0000000..3621959 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.credentials.html @@ -0,0 +1,262 @@ + + + + +Python: module ai_core_sdk.credentials + + + + + +
 
ai_core_sdk.credentials
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/credentials.py
+

+

+ + + + + +
 
Modules
       
json
+
os
+
pathlib
+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
CredentialsValue +
Service +
Source +
VCAPEnvironment +
+
+
+

+ + + + + + + +
 
class CredentialsValue(builtins.object)
   CredentialsValue(
+    name: 'str',
+    vcap_key: 'Optional[Tuple[str, ...]]' = None,
+    transform_fn: 'Optional[Callable]' = None
+) -&gt; None

+CredentialsValue(name: 'str', vcap_key: 'Optional[Tuple[str, ...]]' = None, transform_fn: 'Optional[Callable]' = None)
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__( + self, + name: 'str', + vcap_key: 'Optional[Tuple[str, ...]]' = None, + transform_fn: 'Optional[Callable]' = None +) -> None
Initialize self.  See help(type(self)) for accurate signature.
+ +
__replace__ = _replace(self, /, **changes) from dataclasses
+ +
__repr__(self)
Return repr(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__annotations__ = {'name': 'str', 'transform_fn': 'Optional[Callable]', 'vcap_key': 'Optional[Tuple[str, ...]]'}
+ +
__dataclass_fields__ = {'name': Field(name='name',type='str',default=<dataclasse...appingproxy({}),kw_only=False,_field_type=_FIELD), 'transform_fn': Field(name='transform_fn',type='Optional[Callabl...appingproxy({}),kw_only=False,_field_type=_FIELD), 'vcap_key': Field(name='vcap_key',type='Optional[Tuple[str, ...appingproxy({}),kw_only=False,_field_type=_FIELD)}
+ +
__dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,ord...rue,kw_only=False,slots=False,weakref_slot=False)
+ +
__hash__ = None
+ +
__match_args__ = ('name', 'vcap_key', 'transform_fn')
+ +
transform_fn = None
+ +
vcap_key = None
+ +

+ + + + + + + +
 
class Service(builtins.object)
   Service(env: 'Dict[str, Any]')

+
 
 Methods defined here:
+
__getitem__(self, key)
+ +
__init__(self, env: 'Dict[str, Any]')
Initialize self.  See help(type(self)) for accurate signature.
+ +
get(self, key, default=NoDefault)
+ +
+Readonly properties defined here:
+
label
+
+
name
+
+
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class Source(builtins.object)
   Source(name: 'str', get: 'Callable[[CredentialsValue], Optional[str]]') -&gt; None

+Source(name: 'str', get: 'Callable[[CredentialsValue], Optional[str]]')
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: 'str', get: 'Callable[[CredentialsValue], Optional[str]]') -> None
Initialize self.  See help(type(self)) for accurate signature.
+ +
__replace__ = _replace(self, /, **changes) from dataclasses
+ +
__repr__(self)
Return repr(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__annotations__ = {'get': 'Callable[[CredentialsValue], Optional[str]]', 'name': 'str'}
+ +
__dataclass_fields__ = {'get': Field(name='get',type='Callable[[CredentialsValu...appingproxy({}),kw_only=False,_field_type=_FIELD), 'name': Field(name='name',type='str',default=<dataclasse...appingproxy({}),kw_only=False,_field_type=_FIELD)}
+ +
__dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,ord...rue,kw_only=False,slots=False,weakref_slot=False)
+ +
__hash__ = None
+ +
__match_args__ = ('name', 'get')
+ +

+ + + + + + + +
 
class VCAPEnvironment(builtins.object)
   VCAPEnvironment(services: 'List[Service]') -&gt; None

+VCAPEnvironment(services: 'List[Service]')
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__getitem__(self, name) -> 'Service'
+ +
__init__(self, services: 'List[Service]') -> None
Initialize self.  See help(type(self)) for accurate signature.
+ +
__replace__ = _replace(self, /, **changes) from dataclasses
+ +
__repr__(self)
Return repr(self).
+ +
get_service(self, label, exactly_one: 'bool' = True) -> 'Service'
+ +
get_service_by_name(self, name, exactly_one: 'bool' = True) -> 'Service'
+ +
+Class methods defined here:
+
from_dict(env: 'Dict[str, Any]')
+ +
from_env(env_var: 'Optional[str]' = None)
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__annotations__ = {'services': 'List[Service]'}
+ +
__dataclass_fields__ = {'services': Field(name='services',type='List[Service]',defau...appingproxy({}),kw_only=False,_field_type=_FIELD)}
+ +
__dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,ord...rue,kw_only=False,slots=False,weakref_slot=False)
+ +
__hash__ = None
+ +
__match_args__ = ('services',)
+ +

+ + + + + +
 
Functions
       
fetch_credentials( + profile: 'str' = None, + credential_values: 'List[CredentialsValue]' = [CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid'), transform_fn=None), CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret'), transform_fn=None), CredentialsValue(name='auth_url', vcap_key=('credentials', 'url'), transform_fn=<lambda>), CredentialsValue(name='base_url', vcap_key=('credentials', 'serviceurls', 'AI_API_URL'), transform_fn=<lambda>), CredentialsValue(name='resource_group', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'), transform_fn=<lambda>), CredentialsValue(name='cert_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='key_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'), transform_fn=<lambda>), CredentialsValue(name='key_str', vcap_key=('credentials', 'key'), transform_fn=<lambda>)], + validate: 'bool' = True, + **kwargs +) -> 'Dict[str, str]'
Fetch credentials from a single source based on precedence.

+Precedence order: kwargs > environment variables > config file > VCAP service

+Once a source is selected (first one with any credential), all credentials
+come from that source only. Resource group is an exception and follows
+precedence independently.

+If credential_values is provided and it's not extended from the CORE_CREDENTIAL_VALUES, set validate to False
+
get_nested_value(data_dict, keys: 'List[str]')
Retrieve a nested value from a dictionary using a list of strings.

+:param data_dict: The dictionary to search.
+:param keys: A list of strings representing nested keys.
+:return: The value associated with the nested keys, or None if not found.
+
init_conf(profile: 'str' = None)
+
resolve_resource_group(sources: 'List[Source]') -> 'Optional[str]'
Find resource_group from the first source that defines it.
+
validate_credentials(credentials: 'Dict[str, str]') -> 'None'
Validate that we have a complete authentication method.
+

+ + + + + +
 
Data
       AI_CORE_PREFIX = 'AICORE'
+AUTH_ENDPOINT_SUFFIX = '/oauth/token'
+CONFIG_FILE_ENV_VAR = 'AICORE_CONFIG'
+CORE_CREDENTIAL_VALUES = [CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid'), transform_fn=None), CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret'), transform_fn=None), CredentialsValue(name='auth_url', vcap_key=('credentials', 'url'), transform_fn=<lambda>), CredentialsValue(name='base_url', vcap_key=('cre...rviceurls', 'AI_API_URL'), transform_fn=<lambda>), CredentialsValue(name='resource_group', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'), transform_fn=<lambda>), CredentialsValue(name='cert_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='key_file_path', vcap_key=None, transform_fn=None), CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'), transform_fn=<lambda>), CredentialsValue(name='key_str', vcap_key=('credentials', 'key'), transform_fn=<lambda>)]
+Callable = typing.Callable
+Dict = typing.Dict
+Final = typing.Final
+List = typing.List
+NoDefault = NoDefault
+Optional = typing.Optional
+PROFILE_ENV_VAR = 'AICORE_PROFILE'
+Tuple = typing.Tuple
+VCAP_AICORE_SERVICE_NAME = 'aicore'
+VCAP_SERVICES_ENV_VAR = 'VCAP_SERVICES'
+__annotations__ = {'CORE_CREDENTIAL_VALUES': 'Final[List[CredentialsValue]]'}
+logger = <Logger ai_core_sdk (INFO)>
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.exception.html b/packages/core/docs/ai_core_sdk.exception.html new file mode 100644 index 0000000..8005afe --- /dev/null +++ b/packages/core/docs/ai_core_sdk.exception.html @@ -0,0 +1,156 @@ + + + + +Python: module ai_core_sdk.exception + + + + + +
 
ai_core_sdk.exception
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/exception.py
+

+

+ + + + + +
 
Classes
       
+
builtins.Exception(builtins.BaseException) +
+
+
AICoreSDKException +
+
+
AICoreInvalidInputException +
+
+
+
+
+

+ + + + + + + +
 
class AICoreInvalidInputException(AICoreSDKException)
   Exception thrown in case of invalid input
 
 
Method resolution order:
+
AICoreInvalidInputException
+
AICoreSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Data descriptors inherited from AICoreSDKException:
+
__weakref__
+
list of weak references to the object
+
+
+Methods inherited from builtins.Exception:
+
__init__(self, /, *args, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + + + +
 
class AICoreSDKException(builtins.Exception)
   Base Exception class for AI Core SDK exceptions
 
 
Method resolution order:
+
AICoreSDKException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Data descriptors defined here:
+
__weakref__
+
list of weak references to the object
+
+
+Methods inherited from builtins.Exception:
+
__init__(self, /, *args, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) class method of builtins.Exception
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(self, /)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setstate__(self, object, /)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(self, object, /)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(self, object, /)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.helpers.constants.html b/packages/core/docs/ai_core_sdk.helpers.constants.html new file mode 100644 index 0000000..ed3acc2 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.helpers.constants.html @@ -0,0 +1,101 @@ + + + + +Python: module ai_core_sdk.helpers.constants + + + + + +
 
ai_core_sdk.helpers.constants
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/helpers/constants.py
+

+

+ + + + + +
 
Modules
       
os
+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
Timeouts +
+
+
+

+ + + + + + + +
 
class Timeouts(enum.Enum)
   Timeouts(*values)

+
 
 
Method resolution order:
+
Timeouts
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
NUM_REQUEST_RETRIES = <Timeouts.NUM_REQUEST_RETRIES: 3>
+ +
READ_TIMEOUT = <Timeouts.READ_TIMEOUT: 60>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ + + + + +
 
Data
       AI_CORE_PREFIX = 'AICORE'
+AUTH_ENDPOINT_SUFFIX = '/oauth/token'
+CONFIG_FILE_ENV_VAR = 'AICORE_CONFIG'
+DEBUG_ENV_VAR_NAME = 'DEBUG'
+DEFAULT_HOME_PATH = '/home/runner/.aicore'
+HOME_PATH_ENV_VAR = 'AICORE_HOME'
+PROFILE_ENV_VAR = 'AICORE_PROFILE'
+VCAP_AICORE_SERVICE_NAME = 'aicore'
+VCAP_SERVICES_ENV_VAR = 'VCAP_SERVICES'
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.helpers.html b/packages/core/docs/ai_core_sdk.helpers.html new file mode 100644 index 0000000..861db41 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.helpers.html @@ -0,0 +1,49 @@ + + + + +Python: package ai_core_sdk.helpers + + + + + +
 
ai_core_sdk.helpers
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/helpers/__init__.py
+

+

+ + + + + +
 
Package Contents
       
constants
+
logging
+

+ + + + + +
 
Functions
       
form_top_skip_params(top: int = None, skip: int = None) -> Dict[str, int]
Frame query param

+:param top: Number of objects to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of objects to be skipped, from the list of the queried objects,
+    defaults to None
+:type skip: int, optional
+
get_home() -> str
+
is_within_aicore() -> bool
[summary]
+Function to check whether the sdk is used within or out of aicore cluster
+Returns:
+    bool: True if the ai-core-sdk is used within aicore cluster
+          False if the ai-core-sdk is used outside aicore cluster
+

+ + + + + +
 
Data
       DEFAULT_HOME_PATH = '/home/runner/.aicore'
+Dict = typing.Dict
+HOME_PATH_ENV_VAR = 'AICORE_HOME'
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.helpers.logging.html b/packages/core/docs/ai_core_sdk.helpers.logging.html new file mode 100644 index 0000000..aad6177 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.helpers.logging.html @@ -0,0 +1,38 @@ + + + + +Python: module ai_core_sdk.helpers.logging + + + + + +
 
ai_core_sdk.helpers.logging
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/helpers/logging.py
+

+

+ + + + + +
 
Modules
       
logging
+
os
+

+ + + + + +
 
Functions
       
get_logger(name: str = None)
+
set_log_level(logger: logging.Logger, default_level=20)
+

+ + + + + +
 
Data
       BASE_LOGGER_NAME = 'ai_core_sdk'
+DEBUG_ENV_VAR_NAME = 'DEBUG'
+DEFAULT_LOG_LEVEL = 20
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.html b/packages/core/docs/ai_core_sdk.html new file mode 100644 index 0000000..94f1d13 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.html @@ -0,0 +1,28 @@ + + + + +Python: package ai_core_sdk + + + + + +
 
ai_core_sdk
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/__init__.py
+

+

+ + + + + +
 
Package Contents
       
ai_core_v2_client
+cli
+
credentials
+exception
+
helpers (package)
+models (package)
+
resource_clients (package)
+tracking (package)
+
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.application.html b/packages/core/docs/ai_core_sdk.models.application.html new file mode 100644 index 0000000..14dc2be --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.application.html @@ -0,0 +1,89 @@ + + + + +Python: module ai_core_sdk.models.application + + + + + +
 
ai_core_sdk.models.application
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/application.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Application +
+
+
+

+ + + + + + + +
 
class Application(builtins.object)
   Application(
+    path: str,
+    revision: str,
+    repository_url: str,
+    application_name: str,
+    **kwargs
+)

+The Application object defines the application.

+:param path: path within the repository
+:type path: str
+:param revision: revision
+:type revision: str
+:param repository_url: URL of the repository
+:type repository_url: str
+:param application_name: name of the application
+:type application_name: str
 
 Methods defined here:
+
__init__( + self, + path: str, + revision: str, + repository_url: str, + application_name: str, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(application_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.application.Applicationobject, created from the values in
+the dict provided as parameter

+:param application_dict: Dict which includes the necessary values to create the object
+:type application_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.application.Application`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.application_query_response.html b/packages/core/docs/ai_core_sdk.models.application_query_response.html new file mode 100644 index 0000000..d51cc5e --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.application_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.application_query_response + + + + + +
 
ai_core_sdk.models.application_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/application_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ApplicationQueryResponse +
+
+
+

+ + + + + + + +
 
class ApplicationQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ApplicationQueryResponse(
+    resources: List[ai_core_sdk.models.application.Application],
+    count: int,
+    **kwargs
+)

+The ApplicationQueryResponse object defines the response of the applications query request
+:param resources: List of the applications returned from the server
+:type resources: List[class:`ai_core_sdk.models.application.Application`]
+:param count: Total number of the queried applications
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ApplicationQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.application.Application], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.application_resource_sync_status.html b/packages/core/docs/ai_core_sdk.models.application_resource_sync_status.html new file mode 100644 index 0000000..df97119 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.application_resource_sync_status.html @@ -0,0 +1,87 @@ + + + + +Python: module ai_core_sdk.models.application_resource_sync_status + + + + + +
 
ai_core_sdk.models.application_resource_sync_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/application_resource_sync_status.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ApplicationResourceSyncStatus +
+
+
+

+ + + + + + + +
 
class ApplicationResourceSyncStatus(builtins.object)
   ApplicationResourceSyncStatus(
+    name: str = None,
+    kind: str = None,
+    status: str = None,
+    message: str = None,
+    **kwargs
+)

+The ApplicationSyncResourcesStatus object defines the status of sync of application resource.

+:param name: Name of the application resource, defaults to None
+:type name: str, optional
+:param kind: kind of the application resource
+:type kind: str, optional
+:param status: status of the sync of the application resource
+:type status: str, optional
+:param message: application resource message
+:type message: str, optional
 
 Methods defined here:
+
__init__( + self, + name: str = None, + kind: str = None, + status: str = None, + message: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(app_res_sync_status_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus`
+object, created from the values in the dict provided as parameter

+:param app_res_sync_status_dict: Dict which includes the necessary values to create the object
+:type app_res_sync_status_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.application_source.html b/packages/core/docs/ai_core_sdk.models.application_source.html new file mode 100644 index 0000000..43be580 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.application_source.html @@ -0,0 +1,85 @@ + + + + +Python: module ai_core_sdk.models.application_source + + + + + +
 
ai_core_sdk.models.application_source
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/application_source.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ApplicationSource +
+
+
+

+ + + + + + + +
 
class ApplicationSource(builtins.object)
   ApplicationSource(
+    repo_url: str = None,
+    path: str = None,
+    revision: str = None,
+    **kwargs
+)

+The ApplicationSource object defines the application source.

+:param repo_url: URL of the repository, defaults to None
+:type repo_url: str, optional
+:param path: path within the repository, defaults to None
+:type path: str, optional
+:param revision: revision number of the application, defaults to None
+:type revision: str, optional
 
 Methods defined here:
+
__init__( + self, + repo_url: str = None, + path: str = None, + revision: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(application_source_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.application_source.ApplicationSourceobject, created from the values in
+the dict provided as parameter

+:param application_source_dict: Dict which includes the necessary values to create the object
+:type application_source_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.application_source.ApplicationSource`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.application_status.html b/packages/core/docs/ai_core_sdk.models.application_status.html new file mode 100644 index 0000000..c5ab56a --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.application_status.html @@ -0,0 +1,107 @@ + + + + +Python: module ai_core_sdk.models.application_status + + + + + +
 
ai_core_sdk.models.application_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/application_status.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ApplicationStatus +
+
+
+

+ + + + + + + +
 
class ApplicationStatus(builtins.object)
   ApplicationStatus(
+    health_status: str = None,
+    sync_status: str = None,
+    message: str = None,
+    source: ai_core_sdk.models.application_source.ApplicationSource = None,
+    sync_finished_at: str = None,
+    sync_started_at: str = None,
+    reconciled_at: str = None,
+    sync_resources_status: List[ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus] = None,
+    **kwargs
+)

+The Application object defines the application.

+:param health_status: Application health status, defaults to None
+:type health_status: str, optional
+:param sync_status: Application sync status, defaults to None
+:type sync_status: str, optional
+:param message: Application health status message, defaults to None
+:type message: str, optional
+:param source: Application source, defaults to None
+:type source: class:`ai_core_sdk.models.application_status.ApplicationStatus`, optional
+:param sync_finished_at: Application sync finish time, defaults to None
+:type sync_finished_at: str, optional
+:param sync_started_at: Application sync start time, defaults to None
+:type sync_started_at: str, optional
+:param reconciled_at: Application reconciliation time, defaults to None
+:type reconciled_at: str, optional
+:param sync_resources_status: Status of the synchronization of the application resources, defaults to None
+:type sync_resources_status:
+    List[class:`ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus`], optional
 
 Methods defined here:
+
__init__( + self, + health_status: str = None, + sync_status: str = None, + message: str = None, + source: ai_core_sdk.models.application_source.ApplicationSource = None, + sync_finished_at: str = None, + sync_started_at: str = None, + reconciled_at: str = None, + sync_resources_status: List[ai_core_sdk.models.application_resource_sync_status.ApplicationResourceSyncStatus] = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(application_status_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.application_status.ApplicationStatusobject, created from the values in
+the dict provided as parameter

+:param application_status_dict: Dict which includes the necessary values to create the object
+:type application_status_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.application_status.ApplicationStatus`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.base_models.html b/packages/core/docs/ai_core_sdk.models.base_models.html new file mode 100644 index 0000000..7c108f3 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.base_models.html @@ -0,0 +1,116 @@ + + + + +Python: module ai_core_sdk.models.base_models + + + + + +
 
ai_core_sdk.models.base_models
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/base_models.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
BasicNameResponse +
Message +
+
+
+

+ + + + + + + +
 
class BasicNameResponse(builtins.object)
   BasicNameResponse(name: str, message: str, **kwargs)

+The BasicNameResponse object defines the response with name from the server

+:param name: Name of the relevant resource
+:type id: str
+:param message: Response message from the server
+:type message: str
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 Methods defined here:
+
__init__(self, name: str, message: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(bnr_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.base_models.BasicNameResponseobject, created from the values in the
+dict provided as parameter

+:param bnr_dict: Dict which includes the necessary values to create the object
+:type bnr_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class Message(builtins.object)
   Message(message: str, **kwargs)

+Message object defines a message

+:param message: message
+:type message: str
 
 Methods defined here:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, message: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(message_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.base_models.Messageobject, created from the values in the
+dict provided as parameter

+:param message_dict: Dict which includes the necessary values to create the object
+:type message_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.base_models.Message`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes defined here:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.docker_registry_secret.html b/packages/core/docs/ai_core_sdk.models.docker_registry_secret.html new file mode 100644 index 0000000..3d48ab5 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.docker_registry_secret.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_core_sdk.models.docker_registry_secret + + + + + +
 
ai_core_sdk.models.docker_registry_secret
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/docker_registry_secret.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.Name(builtins.object) +
+
+
DockerRegistrySecret +
+
+
+

+ + + + + + + +
 
class DockerRegistrySecret(ai_api_client_sdk.models.base_models.Name)
   DockerRegistrySecret(name: str, **kwargs)

+The DockerRegistrySecret object defines the docker registry secret. Refer to
+:class:`ai_api_client_sdk.models.base_models.Name`, for the object definition
 
 
Method resolution order:
+
DockerRegistrySecret
+
ai_api_client_sdk.models.base_models.Name
+
builtins.object
+
+
+Methods defined here:
+
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(docker_registry_secret_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret` object, created
+from the values in the dict provided as parameter

+:param docker_registry_secret_dict: Dict which includes the necessary values to create the object
+:type docker_registry_secret_dict: Dict[str, str]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.Name:
+
__eq__(self, other)
Return self==value.
+ +
__init__(self, name: str, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.Name:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.models.base_models.Name:
+
__hash__ = None
+ +

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.docker_registry_secret_query_response.html b/packages/core/docs/ai_core_sdk.models.docker_registry_secret_query_response.html new file mode 100644 index 0000000..59b001f --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.docker_registry_secret_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.docker_registry_secret_query_response + + + + + +
 
ai_core_sdk.models.docker_registry_secret_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/docker_registry_secret_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
DockerRegistrySecretQueryResponse +
+
+
+

+ + + + + + + +
 
class DockerRegistrySecretQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   DockerRegistrySecretQueryResponse(
+    resources: List[ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret],
+    count: int,
+    **kwargs
+)

+The DockerRegistrySecretQueryResponse object defines the response of the dockerRegistrySecrets query request
+:param resources: List of the docker registry secrets returned from the server
+:type resources: List[class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret`]
+:param count: Total number of the queried docker registry secrets
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
DockerRegistrySecretQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.html b/packages/core/docs/ai_core_sdk.models.html new file mode 100644 index 0000000..5d3dc2d --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.html @@ -0,0 +1,39 @@ + + + + +Python: package ai_core_sdk.models + + + + + +
 
ai_core_sdk.models
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/__init__.py
+

+

+ + + + + +
 
Package Contents
       
application
+application_query_response
+application_resource_sync_status
+application_source
+application_status
+
base_models
+docker_registry_secret
+docker_registry_secret_query_response
+kpi
+object_store_secret
+
object_store_secret_query_response
+repository
+repository_query_response
+repository_status
+resource_group
+
resource_group_query_response
+resource_group_status
+secret
+secret_query_response
+
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.kpi.html b/packages/core/docs/ai_core_sdk.models.kpi.html new file mode 100644 index 0000000..3391963 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.kpi.html @@ -0,0 +1,69 @@ + + + + +Python: module ai_core_sdk.models.kpi + + + + + +
 
ai_core_sdk.models.kpi
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/kpi.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Kpi +
+
+
+

+ + + + + + + +
 
class Kpi(builtins.object)
   Kpi(header: List[str], rows: List[Union[str, int]], **kwargs)

+The Kpi object defines the Kpi data.
 
 Methods defined here:
+
__init__(self, header: List[str], rows: List[Union[str, int]], **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(kpi_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.kpi.Kpiobject, created
+from the values in the dict provided as parameter

+:param kpi_dict: Dict which includes the necessary values to create the object
+:type kpi_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.kpi.Kpi`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+Union = typing.Union
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.object_store_secret.html b/packages/core/docs/ai_core_sdk.models.object_store_secret.html new file mode 100644 index 0000000..28d2cb4 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.object_store_secret.html @@ -0,0 +1,67 @@ + + + + +Python: module ai_core_sdk.models.object_store_secret + + + + + +
 
ai_core_sdk.models.object_store_secret
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/object_store_secret.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ObjectStoreSecret +
+
+
+

+ + + + + + + +
 
class ObjectStoreSecret(builtins.object)
   ObjectStoreSecret(name: str, metadata: Dict[str, str], **kwargs)

+The ObjectStoreSecret object defines the object store secret response.
 
 Methods defined here:
+
__init__(self, name: str, metadata: Dict[str, str], **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(object_store_secret_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecretobject, created
+from the values in the dict provided as parameter

+:param object_store_secret_dict: Dict which includes the necessary values to create the object
+:type object_store_secret_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.object_store_secret_query_response.html b/packages/core/docs/ai_core_sdk.models.object_store_secret_query_response.html new file mode 100644 index 0000000..0eda720 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.object_store_secret_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.object_store_secret_query_response + + + + + +
 
ai_core_sdk.models.object_store_secret_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/object_store_secret_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ObjectStoreSecretQueryResponse +
+
+
+

+ + + + + + + +
 
class ObjectStoreSecretQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ObjectStoreSecretQueryResponse(
+    resources: List[ai_core_sdk.models.object_store_secret.ObjectStoreSecret],
+    count: int,
+    **kwargs
+)

+The ObjectStoreSecretQueryResponse object defines the response of the object store secret query request
+:param resources: List of the object store secrets returned from the server
+:type resources: List[class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret`]
+:param count: Total number of the queried object store secrets
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ObjectStoreSecretQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.object_store_secret.ObjectStoreSecret], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.repository.html b/packages/core/docs/ai_core_sdk.models.repository.html new file mode 100644 index 0000000..826135a --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.repository.html @@ -0,0 +1,85 @@ + + + + +Python: module ai_core_sdk.models.repository + + + + + +
 
ai_core_sdk.models.repository
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/repository.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Repository +
+
+
+

+ + + + + + + +
 
class Repository(builtins.object)
   Repository(
+    name: str,
+    url: str,
+    status: ai_core_sdk.models.repository_status.RepositoryStatus = None,
+    **kwargs
+)

+The Repository object defines the repository

+:param name: name of the repository
+:type name: str
+:param url: URL of the repository
+:type url: str
+:param status: status of the repository, defaults to None
+:type status: class:`ai_core_sdk.models.repository_status.RepositoryStatus`, optional
 
 Methods defined here:
+
__init__( + self, + name: str, + url: str, + status: ai_core_sdk.models.repository_status.RepositoryStatus = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(repository_dict: Dict[str, str])
Returns a :class:`ai_core_sdk.models.repository.Repositoryobject, created
+from the values in the dict provided as parameter

+:param repository_dict: Dict which includes the necessary values to create the object
+:type repository_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.repository.Repository`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.repository_query_response.html b/packages/core/docs/ai_core_sdk.models.repository_query_response.html new file mode 100644 index 0000000..3c356ed --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.repository_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.repository_query_response + + + + + +
 
ai_core_sdk.models.repository_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/repository_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
RepositoryQueryResponse +
+
+
+

+ + + + + + + +
 
class RepositoryQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   RepositoryQueryResponse(
+    resources: List[ai_core_sdk.models.repository.Repository],
+    count: int,
+    **kwargs
+)

+The RepositoryQueryResponse object defines the response of the repository query request
+:param resources: List of the repositories returned from the server
+:type resources: List[class:`ai_core_sdk.models.repository.Repository`]
+:param count: Total number of the queried repositories
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
RepositoryQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.repository.Repository], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_client_sdk.models.repository_query_response.RepositoryQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.repository_query_response.RepositoryQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.repository_status.html b/packages/core/docs/ai_core_sdk.models.repository_status.html new file mode 100644 index 0000000..b3e73cc --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.repository_status.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_core_sdk.models.repository_status + + + + + +
 
ai_core_sdk.models.repository_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/repository_status.py
+

+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
RepositoryStatus +
+
+
+

+ + + + + + + +
 
class RepositoryStatus(enum.Enum)
   RepositoryStatus(*values)

+RepositoryStatus is an Enum defining the valid values of the status of a repository
 
 
Method resolution order:
+
RepositoryStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
COMPLETED = <RepositoryStatus.COMPLETED: 'COMPLETED'>
+ +
ERROR = <RepositoryStatus.ERROR: 'ERROR'>
+ +
IN_PROGRESS = <RepositoryStatus.IN_PROGRESS: 'IN-PROGRESS'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.resource_group.html b/packages/core/docs/ai_core_sdk.models.resource_group.html new file mode 100644 index 0000000..b50ed3c --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.resource_group.html @@ -0,0 +1,98 @@ + + + + +Python: module ai_core_sdk.models.resource_group + + + + + +
 
ai_core_sdk.models.resource_group
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/resource_group.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
ResourceGroup +
+
+
+

+ + + + + + + +
 
class ResourceGroup(builtins.object)
   ResourceGroup(
+    resource_group_id: str = None,
+    tenant_id: str = None,
+    zone_id: str = None,
+    labels: List[ai_api_client_sdk.models.label.Label] = None,
+    status: ai_core_sdk.models.resource_group_status.ResourceGroupStatus = None,
+    status_message: str = None,
+    **kwargs
+)

+ResourceGroup represents the resource group.

+:param resource_group_id: The resource_group_id of this ResourceGroup.
+:type resource_group_id: str
+:param tenant_id: The tenant_id of this ResourceGroup.
+:type tenant_id: str
+:param zone_id: The zone_id of this ResourceGroup.
+:type zone_id: str
+:param labels: The labels of this ResourceGroup.
+:type labels: ResourceGroupLabels
+:param status: The status of this ResourceGroup.
+:type status: str
+:param status_message: The status_message of this ResourceGroup.
+:type status_message: str
 
 Methods defined here:
+
__init__( + self, + resource_group_id: str = None, + tenant_id: str = None, + zone_id: str = None, + labels: List[ai_api_client_sdk.models.label.Label] = None, + status: ai_core_sdk.models.resource_group_status.ResourceGroupStatus = None, + status_message: str = None, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(resource_group_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.resource_group.ResourceGroupobject, created
+from the values in the dict provided as parameter

+:param resource_group_dict: Dict which includes the necessary values to create the object
+:type resource_group_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.resource_group.ResourceGroup`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.resource_group_query_response.html b/packages/core/docs/ai_core_sdk.models.resource_group_query_response.html new file mode 100644 index 0000000..d56334f --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.resource_group_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.resource_group_query_response + + + + + +
 
ai_core_sdk.models.resource_group_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/resource_group_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
ResourceGroupQueryResponse +
+
+
+

+ + + + + + + +
 
class ResourceGroupQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   ResourceGroupQueryResponse(
+    resources: List[ai_core_sdk.models.resource_group.ResourceGroup],
+    count: int,
+    **kwargs
+)

+The ResourceGroupQueryResponse object defines the response of the resourceGroups query request
+:param resources: List of the resource groups returned from the server
+:type resources: List[class:`ai_core_sdk.models.resource_group.ResourceGroup`]
+:param count: Total number of the queried docker registry secrets
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
ResourceGroupQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.resource_group.ResourceGroup], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.resource_group_status.html b/packages/core/docs/ai_core_sdk.models.resource_group_status.html new file mode 100644 index 0000000..976e79e --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.resource_group_status.html @@ -0,0 +1,82 @@ + + + + +Python: module ai_core_sdk.models.resource_group_status + + + + + +
 
ai_core_sdk.models.resource_group_status
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/resource_group_status.py
+

+

+ + + + + +
 
Classes
       
+
enum.Enum(builtins.object) +
+
+
ResourceGroupStatus +
+
+
+

+ + + + + + + +
 
class ResourceGroupStatus(enum.Enum)
   ResourceGroupStatus(*values)

+ResourceGroupStatus is an Enum defining the valid values of the status of a resource group
 
 
Method resolution order:
+
ResourceGroupStatus
+
enum.Enum
+
builtins.object
+
+
+Data and other attributes defined here:
+
ERROR = <ResourceGroupStatus.ERROR: 'ERROR'>
+ +
PROVISIONED = <ResourceGroupStatus.PROVISIONED: 'PROVISIONED'>
+ +
PROVISIONING = <ResourceGroupStatus.PROVISIONING: 'PROVISIONING'>
+ +
+Data descriptors inherited from enum.Enum:
+
name
+
The name of the Enum member.
+
+
value
+
The value of the Enum member.
+
+
+Static methods inherited from enum.EnumType:
+
__contains__(value)
Return True if `value` is in `cls`.

+`value` is in `cls` if:
+1) `value` is a member of `cls`, or
+2) `value` is the value of one of the `cls`'s members.
+3) `value` is a pseudo-member (flags)
+ +
__getitem__(name)
Return the member matching `name`.
+ +
__iter__()
Return members in definition order.
+ +
__len__()
Return the number of members (no aliases)
+ +
+Readonly properties inherited from enum.EnumType:
+
__members__
+
Returns a mapping of member name->value.

+This mapping lists all enum members, including aliases.  Note that
+this is a read-only view of the internal mapping.
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.secret.html b/packages/core/docs/ai_core_sdk.models.secret.html new file mode 100644 index 0000000..8b8ba95 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.secret.html @@ -0,0 +1,72 @@ + + + + +Python: module ai_core_sdk.models.secret + + + + + +
 
ai_core_sdk.models.secret
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/secret.py
+

+

+ + + + + +
 
Classes
       
+
builtins.object +
+
+
Secret +
+
+
+

+ + + + + + + +
 
class Secret(builtins.object)
   Secret(name: str, data: Dict[str, str] = None, **kwargs)

+The Secret object defines the secret response.

+:param name: Secret name
+:type name: str
+:param data: Secret data dictionary, defaults to None
+:type data: dict, optional
 
 Methods defined here:
+
__init__(self, name: str, data: Dict[str, str] = None, **kwargs)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Static methods defined here:
+
from_dict(secret_dict: Dict[str, Any])
Returns a :class:`ai_core_sdk.models.secret.Secretobject, created
+from the values in the dict provided as parameter

+:param secret_dict: Dict which includes the necessary values to create the object
+:type secret_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.secret.Secret`
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.models.secret_query_response.html b/packages/core/docs/ai_core_sdk.models.secret_query_response.html new file mode 100644 index 0000000..8efe149 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.models.secret_query_response.html @@ -0,0 +1,91 @@ + + + + +Python: module ai_core_sdk.models.secret_query_response + + + + + +
 
ai_core_sdk.models.secret_query_response
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/models/secret_query_response.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.models.base_models.QueryResponse(builtins.object) +
+
+
SecretQueryResponse +
+
+
+

+ + + + + + + +
 
class SecretQueryResponse(ai_api_client_sdk.models.base_models.QueryResponse)
   SecretQueryResponse(
+    resources: List[ai_core_sdk.models.secret.Secret],
+    count: int,
+    **kwargs
+)

+The SecretQueryResponse object defines the response of the secret query request
+:param resources: List of the secrets returned from the server
+:type resources: List[class:`ai_core_sdk.models.secret.Secret`]
+:param count: Total number of the queried secrets
+:type count: int
+:param `**kwargs`: The keyword arguments are there in case there are additional attributes returned from server
 
 
Method resolution order:
+
SecretQueryResponse
+
ai_api_client_sdk.models.base_models.QueryResponse
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + resources: List[ai_core_sdk.models.secret.Secret], + count: int, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Static methods defined here:
+
from_dict(response_dict: Dict[str, Any])
Returns a
+:class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse`
+object, created from the values in the dict provided as parameter

+:param response_dict: Dict which includes the necessary values to create the object
+:type response_dict: Dict[str, Any]
+:return: An object, created from the values provided
+:rtype: class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__str__(self)
Return str(self).
+ +
+Data descriptors inherited from ai_api_client_sdk.models.base_models.QueryResponse:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Dict = typing.Dict
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.applications_client.html b/packages/core/docs/ai_core_sdk.resource_clients.applications_client.html new file mode 100644 index 0000000..c5ce356 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.applications_client.html @@ -0,0 +1,199 @@ + + + + +Python: module ai_core_sdk.resource_clients.applications_client + + + + + +
 
ai_core_sdk.resource_clients.applications_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/applications_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ApplicationsClient +
+
+
+

+ + + + + + + +
 
class ApplicationsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ApplicationsClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+ApplicationsClient is a class implemented for interacting with the applications related
+endpoints of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ApplicationsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create( + self, + revision: str, + path: str, + application_name: str = None, + repository_name: str = None, + repository_url: str = None +) -> ai_api_client_sdk.models.base_models.BasicResponse
Creates an application.

+:param revision: revision to synchronize
+:type revision: str
+:param path: within the repository to synchronize
+:type path: str
+:param application_name: Name of the application
+:type application_name: str, optional
+:param repository_name: Name of the repository to synchronize. Either this or the repository_url needs to be
+    provided
+:type repository_name: str, optional
+:param repository_url: URL of the repository to synchronize. Either this or the repository_name needs to be
+    provided
+:type repository_url: str, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
delete(self, application_name: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the application.

+:param application_name: name of the application to be deleted
+:type application_name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, application_name: str) -> ai_core_sdk.models.application.Application
Retrieves the application from the server.

+:param application_name: name of the application to be retrieved
+:type application_name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved application
+:rtype: class:`ai_core_sdk.models.application.Application`
+ +
get_status(self, application_name: str) -> ai_core_sdk.models.application_status.ApplicationStatus
Retrieves the application status from the server.

+:param application_name: name of the application
+:type application_name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved application status
+:rtype: class:`ai_core_sdk.models.application_status.ApplicationStatus`
+ +
modify( + self, + application_name: str, + repository_url: str, + path: str, + revision: str +) -> ai_api_client_sdk.models.base_models.BasicResponse
Modifies the application

+:param application_name: name of the application to be modified
+:type name: str
+:param repository_url:
+:type repository_url: str
+:param revision:
+:type revision: str
+:param path:
+:type path: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query(self) -> ai_core_sdk.models.application_query_response.ApplicationQueryResponse
Returns the applications.

+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved applications
+:rtype: class:`ai_core_sdk.models.application_query_response.ApplicationQueryResponse`
+ +
refresh(self, application_name: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Triggers synchronisation of the application.

+:param application_name: name of the application to be refreshed
+:type application_name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.docker_registry_secrets_client.html b/packages/core/docs/ai_core_sdk.resource_clients.docker_registry_secrets_client.html new file mode 100644 index 0000000..cb72541 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.docker_registry_secrets_client.html @@ -0,0 +1,147 @@ + + + + +Python: module ai_core_sdk.resource_clients.docker_registry_secrets_client + + + + + +
 
ai_core_sdk.resource_clients.docker_registry_secrets_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/docker_registry_secrets_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
DockerRegistrySecretsClient +
+
+
+

+ + + + + + + +
 
class DockerRegistrySecretsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   DockerRegistrySecretsClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+DockerRegistrySecretsClient is a class implemented for interacting with the docker registry secret related
+endpoints of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
DockerRegistrySecretsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create(self, name: str, data: dict) -> ai_core_sdk.models.base_models.Message
Creates a docker secret based on the configuration in the request body.

+:param name: name of the docker registry secret
+:type name: str
+:param data: json dict, defining the docker registry secret
+:type data: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_core_sdk.models.base_models.Message`
+ +
delete(self, name: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the docker registry secret with the given name if it exists.

+:param name: name of the docker registry secret to be deleted
+:type name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, name: str) -> ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret
Returns the metadata of the docker registry secrets which matches the given name.

+:param name: name of the docker registry secret to be retrieved
+:type name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved metadata of the docker registry secret
+:rtype: class:`ai_core_sdk.models.docker_registry_secret.DockerRegistrySecret`
+ +
modify(self, name: str, data: dict) -> ai_api_client_sdk.models.base_models.BasicResponse
Updates the docker registry secret

+:param name: name of the docker registry secret to be modified
+:type name: str
+:param data: json dict, defining the docker registry secret
+:type data: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query(self, top: int = None, skip: int = None) -> ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse
Gets a list of metadata of docker registry secrets.

+:param top: Number of docker registry secrets to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of docker registry secrets to be skipped, from the list of the queried docker registry
+    secrets, defaults to None
+:type skip: int, optional
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of metadata of secrets
+:rtype: class:`ai_core_sdk.models.docker_registry_secret_query_response.DockerRegistrySecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.html b/packages/core/docs/ai_core_sdk.resource_clients.html new file mode 100644 index 0000000..58d294e --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.html @@ -0,0 +1,28 @@ + + + + +Python: package ai_core_sdk.resource_clients + + + + + +
 
ai_core_sdk.resource_clients
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/__init__.py
+

+

+ + + + + +
 
Package Contents
       
applications_client
+docker_registry_secrets_client
+
internal_rest_client
+kpi_client
+
metrics_client
+object_store_secrets_client
+
repositories_client
+secrets_client
+
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.internal_rest_client.html b/packages/core/docs/ai_core_sdk.resource_clients.internal_rest_client.html new file mode 100644 index 0000000..a318136 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.internal_rest_client.html @@ -0,0 +1,220 @@ + + + + +Python: module ai_core_sdk.resource_clients.internal_rest_client + + + + + +
 
ai_core_sdk.resource_clients.internal_rest_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/internal_rest_client.py
+

+

+ + + + + +
 
Modules
       
os
+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.helpers.rest_client.RestClient(builtins.object) +
+
+
InternalRestClient +
+
+
+

+ + + + + + + +
 
class InternalRestClient(ai_api_client_sdk.helpers.rest_client.RestClient)
   InternalRestClient(
+    base_url=None,
+    get_token=None,
+    resource_group=None,
+    *args,
+    **kwargs
+)

+InternalRestClient is a class implemented for sending requests to services within aicore. The InternalRestClient should only be used for services that do not require authentication when called within aicore.
 
 
Method resolution order:
+
InternalRestClient
+
ai_api_client_sdk.helpers.rest_client.RestClient
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + base_url=None, + get_token=None, + resource_group=None, + *args, + **kwargs +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Methods inherited from ai_api_client_sdk.helpers.rest_client.RestClient:
+
delete( + self, + path: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a DELETE request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param params: parameters of the request, defaults to None
+:type params: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
get( + self, + path: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + return_bytes_content: bool = False, + **kwargs +) -> Union[dict, int]
Sends a GET request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param params: parameters of the request, defaults to None
+:type params: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param return_bytes_content: expected response.content is bytes
+:type return_bytes_content: bool
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: Union[dict, int]
+ +
patch( + self, + path: str, + body: Dict[str, Union[str, dict, list]], + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a PATCH request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param body: body of the request
+:type body: Dict[str, Union[str, dict, list]]
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
post( + self, + path: str, + body: Dict[str, Union[str, dict]] = None, + headers: Dict[str, str] = None, + resource_group: str = None, + **kwargs +) -> dict
Sends a POST request to the server.

+:param path: path of the endpoint the request should be sent to
+:type path: str
+:param body: body of the request, defaults to None
+:type body: Dict[str, str], optional
+:param headers: headers of the request, defaults to None
+:type headers: Dict[str, str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this, or the
+    resource_group property of this class should be set.
+:type resource_group: str
+:param kwargs: additional keyword arguments to be passed to the request e.g. files, stream, etc.
+:type kwargs: dict
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The JSON response from the server (The keys decamelized)
+:rtype: dict
+ +
+Static methods inherited from ai_api_client_sdk.helpers.rest_client.RestClient:
+
raise_ai_api_exception(error_description, response, response_json)
+ +
+Data descriptors inherited from ai_api_client_sdk.helpers.rest_client.RestClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+
+Data and other attributes inherited from ai_api_client_sdk.helpers.rest_client.RestClient:
+
logger = <Logger ai-api-client-sdk (WARNING)>
+ +

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.kpi_client.html b/packages/core/docs/ai_core_sdk.resource_clients.kpi_client.html new file mode 100644 index 0000000..c1c380c --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.kpi_client.html @@ -0,0 +1,85 @@ + + + + +Python: module ai_core_sdk.resource_clients.kpi_client + + + + + +
 
ai_core_sdk.resource_clients.kpi_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/kpi_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
KpiClient +
+
+
+

+ + + + + + + +
 
class KpiClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   KpiClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+KpiClient is a class implemented for interacting with the analytics kpi
+endpoint of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
KpiClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
query(self) -> ai_core_sdk.models.kpi.Kpi
Retrieves the number of executions, artifacts, and deployments
+for each resource group, scenario, and executable.

+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved KPI data
+:rtype: class:`ai_core_sdk.models.kpi.Kpi`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
delete(self, *args, **kwargs)
Deletes the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
modify(self, *args, **kwargs)
Modifies the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.metrics_client.html b/packages/core/docs/ai_core_sdk.resource_clients.metrics_client.html new file mode 100644 index 0000000..68deb4b --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.metrics_client.html @@ -0,0 +1,215 @@ + + + + +Python: module ai_core_sdk.resource_clients.metrics_client + + + + + +
 
ai_core_sdk.resource_clients.metrics_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/metrics_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.metrics_client.MetricsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient) +
+
+
MetricsCoreClient +
+
+
+

+ + + + + + + +
 
class MetricsCoreClient(ai_api_client_sdk.resource_clients.metrics_client.MetricsClient)
   MetricsCoreClient(rest_client, execution_id: str = None) -&gt; None

+MetricsCoreClient is a class implemented for interacting with the metrics related
+endpoints of the server. It is inherited from the base class
+:class:`ai_api_client_sdk.resource_clients.metrics_client.MetricsClient`
 
 
Method resolution order:
+
MetricsCoreClient
+
ai_api_client_sdk.resource_clients.metrics_client.MetricsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, rest_client, execution_id: str = None) -> None
Initialize self.  See help(type(self)) for accurate signature.
+ +
log_metrics( + self, + metrics: List[ai_api_client_sdk.models.metric.Metric], + execution_id: str = '', + artifact_name: str = None, + resource_group: str = None +) -> None
Creates or updates the metrics for an execution.

+    :param metrics: List of the metrics related to the execution,
+    :type metrics: List[class:`ai_api_client_sdk.metric.Metric`]
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param artifact_name: Name of the artifact to associate with a metric, defaults to None
+    :type artifact_name: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
modify( + self, + execution_id: str = '', + metrics: List[ai_api_client_sdk.models.metric.Metric] = None, + tags: List[ai_api_client_sdk.models.metric_tag.MetricTag] = None, + custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo] = None, + resource_group: str = None +) -> None
Creates or updates the metrics for an execution.

+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param metrics: List of the metrics related to the execution, defaults to None
+    :type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional
+    :param tags: List of the tags related to the execution, defaults to None
+    :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional
+    :param custom_info: List of custom info related to the execution, defaults to None
+    :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
set_custom_info( + self, + custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo], + execution_id: str = '', + resource_group: str = None +) -> None
log custom info against the given execution
+    captures consumption semantics for the metrics or complex metric in JSON format.


+    :param custom_info: List of custom info related to the execution
+    :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
set_tags( + self, + tags: List[ai_api_client_sdk.models.metric_tag.MetricTag], + execution_id: str = '', + resource_group: str = None +) -> None
log tags against the given execution

+    :param tags: List of the tags related to the execution, defaults to None
+    :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag']
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.metrics_client.MetricsClient:
+
delete(self, execution_id: str, resource_group: str = None) -> None
Deletes the metrics.

+:param execution_id: ID of the execution, of which the metrics should be deleted.
+:type execution_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+ +
query( + self, + filter: str = None, + execution_ids: List[str] = None, + select: List[str] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse
Queries the metrics.

+:param filter: Deprecated. Use parameter execution_ids instead. A filter expression that filters the metric
+    resources using execution IDs. User can only use in, eq operators in filter expression, defaults to None
+:type filter: str, optional
+:param execution_ids: IDs of the executions, of which the metrics should be retrieved, defaults to None
+:type execution_ids: List[str], optional
+:param select: Values of select can be metrics,tags,customInfo or any of the combinations of these or *. 
+    Can be used to select(project) only the resources specified
+:type select: List[str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       List = typing.List
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.object_store_secrets_client.html b/packages/core/docs/ai_core_sdk.resource_clients.object_store_secrets_client.html new file mode 100644 index 0000000..659124f --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.object_store_secrets_client.html @@ -0,0 +1,229 @@ + + + + +Python: module ai_core_sdk.resource_clients.object_store_secrets_client + + + + + +
 
ai_core_sdk.resource_clients.object_store_secrets_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/object_store_secrets_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
ObjectStoreSecretsClient +
+
+
+

+ + + + + + + +
 
class ObjectStoreSecretsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   ObjectStoreSecretsClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+ObjectStoreSecretsClient is a class implemented for interacting with the object store secret related
+endpoints of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
ObjectStoreSecretsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create( + self, + name: str, + type: str, + data: dict, + bucket: str = None, + endpoint: str = None, + region: str = None, + path_prefix: str = None, + verifyssl: str = None, + usehttps: str = None, + resource_group: str = None +) -> ai_core_sdk.models.base_models.Message
Creates an object store secret.

+:param name: name of the object store secret
+:type name: str
+:param type: type of object storage
+:type type: str
+:param data: data to be posted
+:type data: str
+:param bucket: name of the bucket
+:type bucket: str
+:param endpoint: endpoint of object storage
+:type endpoint: str
+:param region: region of object storage
+:type region: str
+:param path_prefix: path prefix
+:type path_prefix: str
+:param verifyssl: verify ssl
+:type verifyssl: str
+:param usehttps: use https
+:type usehttps: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIConflictException` if a 409 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of metadata of available secrets
+:rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse`
+ +
delete(self, name: str, resource_group: str = None) -> ai_api_client_sdk.models.base_models.BasicResponse
Deletes the object store secret.

+:param name: name of the object store secret to be deleted
+:type name: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, name: str, resource_group: str = None) -> ai_core_sdk.models.object_store_secret.ObjectStoreSecret
Retrieves the object store secret from the server.

+:param name: name of the object store secret to be retrieved
+:type name: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The retrieved object store secret
+:rtype: class:`ai_core_sdk.models.object_store_secret.ObjectStoreSecret`
+ +
modify( + self, + name: str, + type: str, + data: dict, + bucket: str = None, + endpoint: str = None, + region: str = None, + path_prefix: str = None, + verifyssl: str = None, + usehttps: str = None, + resource_group: str = None +) -> ai_api_client_sdk.models.base_models.BasicResponse
Modifies the object store secret

+:param name: name of the object store secret to be modified
+:type name: str
+:param type: type of object storage
+:type type: str
+:param data: data to be posted
+:type data: str
+:param bucket: name of the bucket
+:type bucket: str
+:param endpoint: endpoint of object storage
+:type endpoint: str
+:param region: region of object storage
+:type region: str
+:param path_prefix: path prefix
+:type path_prefix: str
+:param verifyssl: verify ssl
+:type verifyssl: str
+:param usehttps: use https
+:type usehttps: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query(self, top: int = None, skip: int = None, resource_group: str = None) -> ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse
Returns the object store secrets.

+:param top: Number of object store secrets to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of object store secrets to be skipped, from the list of the queried object store
+    secrets, defaults to None
+:type skip: int, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of object store secrets
+:rtype: class:`ai_core_sdk.models.object_store_secret_query_response.ObjectStoreSecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.repositories_client.html b/packages/core/docs/ai_core_sdk.resource_clients.repositories_client.html new file mode 100644 index 0000000..1798ac8 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.repositories_client.html @@ -0,0 +1,148 @@ + + + + +Python: module ai_core_sdk.resource_clients.repositories_client + + + + + +
 
ai_core_sdk.resource_clients.repositories_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/repositories_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
RepositoriesClient +
+
+
+

+ + + + + + + +
 
class RepositoriesClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   RepositoriesClient(
+    rest_client: ai_api_client_sdk.helpers.rest_client.RestClient
+)

+RepositoriesClient is a class implemented for interacting with the repositories related
+endpoints of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
RepositoriesClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create(self, name: str, url: str, username: str, password: str) -> ai_core_sdk.models.base_models.Message
On-boards a new GitOps repository

+:param name: name of the GitOps repository
+:type name: str
+:param url: url of the GitOps repository
+:type url: str
+:param username: username to the GitOps repository
+:type username: str
+:param password: password to the GitOps repository
+:type password: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_core_sdk.models.base_models.BasicNameResponse`
+ +
delete(self, name: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Off-boards a GitOps repository.

+:param name: name of the repository to be deleted
+:type name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
get(self, name: str) -> ai_core_sdk.models.repository.Repository
Retrieves the access details for a repository if it exists.

+:param name: name of the repository to be retrieved
+:type name: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: The access details for a repository
+:rtype: class:`ai_core_client_sdk.models.docker_registry_secret.DockerRegistrySecret`
+ +
modify(self, name: str, username: str, password: str) -> ai_api_client_sdk.models.base_models.BasicResponse
Updates the referenced repository credentials to synchronize repository.

+:param name: name of the repository to be modified
+:type name: str
+:param username: username to the repository
+:type username: str
+:param password: password to the repository
+:type password: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.BasicResponse`
+ +
query(self) -> ai_core_sdk.models.repository_query_response.RepositoryQueryResponse
Retrieves a list of all GitOps repositories for a tenant.

+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list all GitOps repositories for a tenant
+:rtype: class:`ai_core_client_sdk.models.repository_query_response.RepositoryQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.resource_clients.secrets_client.html b/packages/core/docs/ai_core_sdk.resource_clients.secrets_client.html new file mode 100644 index 0000000..5a8415c --- /dev/null +++ b/packages/core/docs/ai_core_sdk.resource_clients.secrets_client.html @@ -0,0 +1,181 @@ + + + + +Python: module ai_core_sdk.resource_clients.secrets_client + + + + + +
 
ai_core_sdk.resource_clients.secrets_client
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/resource_clients/secrets_client.py
+

+

+ + + + + +
 
Classes
       
+
ai_api_client_sdk.resource_clients.base_client.BaseClient(builtins.object) +
+
+
SecretsClient +
+
+
+

+ + + + + + + +
 
class SecretsClient(ai_api_client_sdk.resource_clients.base_client.BaseClient)
   SecretsClient(rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)

+SecretsClient is a class implemented for interacting with the secret related
+endpoints of the server. It implements the base class
+:class:`ai_api_client_sdk.resource_clients.base_client.BaseClient`
 
 
Method resolution order:
+
SecretsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
create( + self, + name: str, + data: dict, + resource_group: str = None, + ai_tenant_scope=True +) -> ai_core_sdk.models.base_models.Message
Creates a secret.

+:param name: name of the secret
+:type name: str
+:param data: data of secret
+:type data: dict
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param ai_tenant_scope: Specify whether the main tenant scope is to be used
+:type ai_tenant_scope: bool
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIConflictException` if a 409 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of metadata of available secrets
+:rtype: class:`ai_core_sdk.models.base_models.Message`
+ +
delete(self, name: str, resource_group: str = None, ai_tenant_scope=True) -> ai_core_sdk.models.base_models.Message
Deletes the secret.

+:param name: name of the secret to be deleted
+:type name: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param ai_tenant_scope: Specify whether the main tenant scope is to be used
+:type ai_tenant_scope: bool
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.Message`
+ +
modify( + self, + name: str, + data: dict, + resource_group: str = None, + ai_tenant_scope=True +) -> ai_core_sdk.models.base_models.Message
Modifies the secret.

+:param name: name of the secret to be modified
+:type name: str
+:param data: data of secret
+:type data: dict
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param ai_tenant_scope: Specify whether the main tenant scope is to be used
+:type ai_tenant_scope: bool
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIPreconditionFailedException` if a 412 response is received from
+    the server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.base_models.Message`
+ +
query( + self, + top: int = None, + skip: int = None, + resource_group: str = None, + ai_tenant_scope: bool = True +) -> ai_core_sdk.models.secret_query_response.SecretQueryResponse
Returns the secrets.

+:param top: Number of secrets to be retrieved, defaults to None
+:type top: int, optional
+:param skip: Number of secrets to be skipped, from the list of the queried secrets, defaults to None
+:type skip: int, optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_core_sdk.ai_core_v2_client.AICoreV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:param ai_tenant_scope: Specify whether the main tenant scope is to be used
+:type ai_tenant_scope: bool
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIForbiddenException` if a 403 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: A list of secrets
+:rtype: class:`ai_core_sdk.models.secret_query_response.SecretQueryResponse`
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__init__(self, rest_client: ai_api_client_sdk.helpers.rest_client.RestClient)
Initialize self.  See help(type(self)) for accurate signature.
+ +
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.tracking.html b/packages/core/docs/ai_core_sdk.tracking.html new file mode 100644 index 0000000..3f6932d --- /dev/null +++ b/packages/core/docs/ai_core_sdk.tracking.html @@ -0,0 +1,21 @@ + + + + +Python: package ai_core_sdk.tracking + + + + + +
 
ai_core_sdk.tracking
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/tracking/__init__.py
+

# pylint: disable=C0114

+

+ + + + + +
 
Package Contents
       
tracking
+
+ \ No newline at end of file diff --git a/packages/core/docs/ai_core_sdk.tracking.tracking.html b/packages/core/docs/ai_core_sdk.tracking.tracking.html new file mode 100644 index 0000000..3bab3a2 --- /dev/null +++ b/packages/core/docs/ai_core_sdk.tracking.tracking.html @@ -0,0 +1,272 @@ + + + + +Python: module ai_core_sdk.tracking.tracking + + + + + +
 
ai_core_sdk.tracking.tracking
index
/home/runner/work/ai-sdk-python/ai-sdk-python/packages/core/ai_core_sdk/tracking/tracking.py
+

# pylint: disable=C0114

+

+ + + + + +
 
Modules
       
os
+

+ + + + + +
 
Classes
       
+
ai_core_sdk.resource_clients.metrics_client.MetricsCoreClient(ai_api_client_sdk.resource_clients.metrics_client.MetricsClient) +
+
+
Tracking +
+
+
+

+ + + + + + + +
 
class Tracking(ai_core_sdk.resource_clients.metrics_client.MetricsCoreClient)
   Tracking(
+    base_url: str = None,
+    auth_url: str = None,
+    client_id: str = None,
+    client_secret: str = None,
+    cert_str: str = None,
+    key_str: str = None,
+    cert_file_path: str = None,
+    key_file_path: str = None,
+    token_creator: Callable[[], str] = None,
+    resource_group: str = None
+)

+Tracking is a class implemented for interacting with the metrics related
+endpoints of the server. It is a wrapper around the base class
+:class:`ai_core_sdk.resource_clients.metrics_client.MetricsCoreClient`

+:param base_url: Base URL of the AI Core. Should include the base path as well. (i.e., "<base_url>/lm/scenarios"
+should work)
+:type base_url: str
+:param auth_url: URL of the authorization endpoint. Should be the full URL (including /oauth/token), defaults to
+    None
+:type auth_url: str, optional
+:param client_id: client id to be used for authorization, defaults to None
+:type client_id: str, optional
+:param client_secret: client secret to be used for authorization, defaults to None
+:type client_secret: str, optional
+:param cert_str: certificate file content, needs to be provided alongside the key_str parameter, defaults to None
+:type cert_str: str, optional
+:param key_str: key file content, needs to be provided alongside the cert_str parameter, defaults to None
+:type key_str: str, optional
+:param cert_file_path: path to the certificate file, needs to be provided alongside the key_file_path parameter,
+    defaults to None
+:type cert_file_path: str, optional
+:param key_file_path: path to the key file, needs to be provided alongside the cert_file_path parameter,
+    defaults to None
+:type key_file_path: str, optional
+:param token_creator: the function which returns the Bearer token, when called. Either this, or
+    auth_url & client_id & client_secret should be specified, defaults to None
+:type token_creator: Callable[[], str], optional
+:param resource_group: The default resource group which will be used while sending the requests to the server. If
+    not set, the resource_group should be specified with every request to the server, defaults to None
+:type resource_group: str, optional
 
 
Method resolution order:
+
Tracking
+
ai_core_sdk.resource_clients.metrics_client.MetricsCoreClient
+
ai_api_client_sdk.resource_clients.metrics_client.MetricsClient
+
ai_api_client_sdk.resource_clients.base_client.BaseClient
+
builtins.object
+
+
+Methods defined here:
+
__init__( + self, + base_url: str = None, + auth_url: str = None, + client_id: str = None, + client_secret: str = None, + cert_str: str = None, + key_str: str = None, + cert_file_path: str = None, + key_file_path: str = None, + token_creator: Callable[[], str] = None, + resource_group: str = None +)
Initialize self.  See help(type(self)) for accurate signature.
+ +
delete(self, execution_id: str, resource_group: str = None) -> None
Deletes the metrics.

+:param execution_id: ID of the execution, of which the metrics should be deleted.
+:type execution_id: str
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPINotFoundException` if a 404 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+ +
log_metrics( + self, + metrics: List[ai_api_client_sdk.models.metric.Metric], + execution_id: str = '', + artifact_name: str = None, + resource_group: str = None +) -> None
Creates or updates the metrics for an execution.

+    :param metrics: List of the metrics related to the execution,
+    :type metrics: List[class:`ai_api_client_sdk.metric.Metric`]
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param artifact_name: Name of the artifact to associate with a metric, defaults to None
+    :type artifact_name: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
modify( + self, + execution_id: str = '', + metrics: List[ai_api_client_sdk.models.metric.Metric] = None, + tags: List[ai_api_client_sdk.models.metric_tag.MetricTag] = None, + custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo] = None, + resource_group: str = None +) -> None
Creates or updates the metrics for an execution.

+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param metrics: List of the metrics related to the execution, defaults to None
+    :type metrics: List[class:`ai_api_client_sdk.metric.Metric`], optional
+    :param tags: List of the tags related to the execution, defaults to None
+    :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag'], optional
+    :param custom_info: List of custom info related to the execution, defaults to None
+    :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
query( + self, + filter: str = None, + execution_ids: List[str] = None, + select: List[str] = None, + resource_group: str = None +) -> ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse
Creates or updates the metrics for an execution.

+:param filter: Deprecated. Use parameter execution_ids instead. A filter expression that filters the metric
+    resources using execution IDs. User can only use in, eq operators in filter expression, defaults to None
+:type filter: str, optional
+:param execution_ids: IDs of the executions, of which the metrics should be retrieved, defaults to None
+:type execution_ids: List[str], optional
+:param select: Values of select can be metrics,tags,customInfo or any of the combinations of these or *.
+    Can be used to select(project) only the resources specified
+:type select: List[str], optional
+:param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+    resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+    defaults to None
+:type resource_group: str
+:raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIAuthorizationException` if a 401 response is received from the
+    server
+:raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+    server
+:return: An object representing the response from the server
+:rtype: class:`ai_api_client_sdk.models.metrics_query_response.MetricsQueryResponse`
+ +
set_custom_info( + self, + custom_info: List[ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo], + execution_id: str = '', + resource_group: str = None +) -> None
log custom info against the given execution
+    captures consumption semantics for the metrics or complex metric in JSON format.


+    :param custom_info: List of custom info related to the execution
+    :type custom_info: List[class:`ai_api_client_sdk.models.metric_custom_info.MetricCustomInfo`], optional
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
set_tags( + self, + tags: List[ai_api_client_sdk.models.metric_tag.MetricTag], + execution_id: str = '', + resource_group: str = None +) -> None
log tags against the given execution

+    :param tags: List of the tags related to the execution, defaults to None
+    :type tags: List[class:'ai_api_client_sdk.models.metric_tag.MetricTag']
+    :param execution_id: ID of the execution, of which the metrics should be modified.
+    :type execution_id: str
+    :param resource_group: Resource Group which the request should be sent on behalf. Either this or a default
+        resource group in the :class:`ai_api_client_sdk.ai_api_v2_client.AIAPIV2Client` should be specified,
+        defaults to None
+    :type resource_group: str
+    :raises: class:`ai_api_client_sdk.exception.AIAPIInvalidRequestException` if a 400 response is received from the
+server
+    :raises: class:`ai_api_client_sdk.exception.AIAPIServerException` if a non-2XX response is received from the
+        server
+ +
+Methods inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
bulk_modify(self, *args, **kwargs)
Modifies multiple instances of the relevant resource. Will be implemented by the respective resource clients
+ +
count(self, *args, **kwargs)
Counts the relevant resources. Will be implemented by the respective resource clients
+ +
create(self, *args, **kwargs)
Creates the relevant resource. Will be implemented by the respective resource clients
+ +
get(self, *args, **kwargs)
Retrieves the relevant resource. Will be implemented by the respective resource clients
+ +
query_logs(self, *args, **kwargs)
Queries the relevant logs. Will be implemented by the respective resource clients
+ +
+Data descriptors inherited from ai_api_client_sdk.resource_clients.base_client.BaseClient:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + +
 
Data
       Callable = typing.Callable
+List = typing.List
+ \ No newline at end of file diff --git a/packages/core/integration_tests/__init__.py b/packages/core/integration_tests/__init__.py new file mode 100644 index 0000000..892b386 --- /dev/null +++ b/packages/core/integration_tests/__init__.py @@ -0,0 +1,114 @@ +import os +import random +import requests +from time import sleep + +from ai_api_client_sdk.helpers.authenticator import Authenticator + + +def get_random_string(l=4): + alphanumeric = 'abcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(random.choices(alphanumeric, k=l)) + + +def get_cluster_info(): + auth_url = os.getenv('XSUAA_AUTH_URL') + client_id = os.getenv('XSUAA_CLIENT_ID') + client_secret = os.getenv('XSUAA_CLIENT_SECRET') + cluster_base_url = os.getenv('CLUSTER_BASE_URL') + base_url = f"{cluster_base_url}/v2" + provisioning_base_url = f'{cluster_base_url}/v2/admin' + return base_url, auth_url, client_id, client_secret, provisioning_base_url + + +def get_s3_bucket_info(): + oss_key = os.getenv('OSS_KEY') + oss_bucket = os.getenv('OSS_BUCKET') + oss_endpoint = os.getenv('OSS_ENDPOINT') + oss_region = os.getenv('OSS_REGION') + oss_secret = os.getenv('OSS_SECRET') + return oss_bucket, oss_endpoint, oss_region, oss_key, oss_secret + + +def get_number_of_integration_tests(): + dir_path = os.path.dirname(__file__) + files = os.listdir(dir_path) + return len(list(filter(lambda x: x.startswith('test'), files))) + + +def get_x509_credentials(): + x509_cert_url = os.getenv(('XSUAA_X509_CERT_URL')) + x509_cert = os.getenv('XSUAA_X509_CERT') + x509_key = os.getenv('XSUAA_X509_KEY') + return x509_cert_url, x509_cert, x509_key + + +TENANT_ID = os.getenv('TEST_TENANT_ID') +RESOURCE_GROUP_ID = f'aicli{get_random_string()}' +BASE_URL, AUTH_URL, CLIENT_ID, CLIENT_SECRET, PROVISIONING_BASE_URL = get_cluster_info() +OSS_BUCKET, OSS_ENDPOINT, OSS_REGION, OSS_KEY, OSS_SECRET = get_s3_bucket_info() +X509_CERT_URL, X509_CERT_STR, X509_KEY_STR = get_x509_credentials() +dir_path = os.path.dirname(__file__) +X509_CERT_FILE_PATH = os.path.join(dir_path, 'x509_cert.pem') +X509_KEY_FILE_PATH = os.path.join(dir_path, 'x509_key.pem') + + +def write_x509_credentials_into_files(): + with open(X509_CERT_FILE_PATH, 'w') as f: + f.write(X509_CERT_STR) + with open(X509_KEY_FILE_PATH, 'w') as f: + f.write(X509_KEY_STR) + + +def remove_x509_credentials(): + os.remove(X509_CERT_FILE_PATH) + os.remove(X509_KEY_FILE_PATH) + +def get_token(): + return Authenticator(auth_url=AUTH_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET).get_token() + + +def provision_resource_group(): + headers = {'Authorization': get_token()} + res = requests.post(url=f'{PROVISIONING_BASE_URL}/resourceGroups', json={"resourceGroupId": RESOURCE_GROUP_ID}, + headers=headers) + if res.status_code // 100 != 2: + raise Exception(f"Failed to create resource group {RESOURCE_GROUP_ID}: {res.status_code}, {res.text}") + + sleep(5) + for i in range(10): + res = requests.get(url=f'{PROVISIONING_BASE_URL}/resourceGroups/{RESOURCE_GROUP_ID}', headers=headers) + try: + if res.status_code == 200 and res.json().get('status') == 'PROVISIONED': + break + except Exception: + pass + sleep(0.5) + headers['AI-Resource-Group'] = RESOURCE_GROUP_ID + res = requests.post(url=f'{PROVISIONING_BASE_URL}/objectStoreSecrets', + json={"name": "default", "type": 'S3', "bucket": OSS_BUCKET, "endpoint": OSS_ENDPOINT, + "pathPrefix": "", "region": OSS_REGION, + "data": {"AWS_ACCESS_KEY_ID": OSS_KEY, "AWS_SECRET_ACCESS_KEY": OSS_SECRET}}, + headers=headers) + if res.status_code // 100 != 2: + raise Exception( + f"Failed to create object store secret for {RESOURCE_GROUP_ID}: {res.status_code}, {res.text}") + + +def deprovision_resource_group(): + headers = {'Authorization': get_token()} + res = requests.delete(url=f'{PROVISIONING_BASE_URL}/resourceGroups/{RESOURCE_GROUP_ID}', headers=headers) + if res.status_code != 202: + raise Exception(f"Failed to remove resource group {RESOURCE_GROUP_ID}") + + +# This function will run before all tests in integration_tests module +def setUpModule(): + provision_resource_group() + write_x509_credentials_into_files() + + +# This function will run after all tests in integration_tests module +def tearDownModule(): + deprovision_resource_group() + remove_x509_credentials() diff --git a/packages/core/integration_tests/ai_core_v2_client_e2e_test_base.py b/packages/core/integration_tests/ai_core_v2_client_e2e_test_base.py new file mode 100644 index 0000000..54c98bd --- /dev/null +++ b/packages/core/integration_tests/ai_core_v2_client_e2e_test_base.py @@ -0,0 +1,50 @@ +import os +from unittest import TestCase +from typing import Any, Dict + +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.credentials import CORE_CREDENTIAL_VALUES +from ai_core_sdk.helpers.constants import AI_CORE_PREFIX +from ai_core_sdk.tracking import Tracking +from . import AUTH_URL, BASE_URL, CLIENT_ID, CLIENT_SECRET, RESOURCE_GROUP_ID, get_random_string + + +class AICoreV2ClientE2ETestBase(TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # Uncomment the following line and the one in tearDownClass to run integration_tests via IDE + # provision_resource_group() + cls.remove_existing_aicore_env_vars() + cls.ai_core_v2_client = AICoreV2Client(base_url=BASE_URL, auth_url=AUTH_URL, client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, resource_group=RESOURCE_GROUP_ID) + cls.tracking_client = Tracking(base_url=BASE_URL, auth_url=AUTH_URL, client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, resource_group=RESOURCE_GROUP_ID) + cls.test_scenario_id = '88888888-4444-4444-4444-cccccccccccc' + + @classmethod + def tearDownClass(cls) -> None: + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # deprovision_resource_group() + super().tearDownClass() + + @staticmethod + def remove_existing_aicore_env_vars(): + for cred_value in CORE_CREDENTIAL_VALUES: + config_name = f'{AI_CORE_PREFIX}_{cred_value.name.upper()}' + if config_name in os.environ.keys(): + del os.environ[config_name] + + def assert_object(self, d: Dict[str, Any], o: object): + for k in d.keys(): + self.assertEqual(d[k], getattr(o, k)) + + @staticmethod + def _get_repo_dict(): + return { + "name": f"test-repo-{get_random_string()}", + "url": f"https://non.existent/bla/bla/{get_random_string()}", + "username": "test_username", + "password": "test_password" + } diff --git a/packages/core/integration_tests/test_e2e_applications.py b/packages/core/integration_tests/test_e2e_applications.py new file mode 100644 index 0000000..5da75c5 --- /dev/null +++ b/packages/core/integration_tests/test_e2e_applications.py @@ -0,0 +1,132 @@ +import copy +from typing import List + +from . import get_random_string +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.application import Application +from ai_core_sdk.models.application_status import ApplicationStatus + + +class TestE2EApplications(AICoreV2ClientE2ETestBase): + + @classmethod + def _create_repo(cls): + cls.repo_dict = cls._get_repo_dict() + cls.ai_core_v2_client.rest_client.post(path='/admin/repositories', body=cls.repo_dict) + + @classmethod + def _delete_repo(cls): + cls.ai_core_v2_client.rest_client.delete(path=f'/admin/repositories/{cls.repo_dict["name"]}') + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls._create_repo() + + @classmethod + def tearDownClass(cls) -> None: + cls._delete_repo() + super().tearDownClass() + + @classmethod + def _get_application_dict(cls): + return { + "repository_url": cls.repo_dict['url'], + "revision": "test_revision", + "path": "test_path", + "application_name": f"test-application-name-{get_random_string()}" + } + + def assert_app_dict_in_app_objects(self, app_dict: dict, apps: List[Application]): + for app in apps: + if app.application_name == app_dict['application_name']: + self.assert_object(app_dict, app) + return + + def assert_app_status(self, app_status: ApplicationStatus): + self.assertTrue(app_status.health_status) + self.assertTrue(app_status.sync_status) + self.assertTrue(app_status.message) + self.assertTrue(app_status.sync_finished_at) + self.assertTrue(app_status.sync_started_at) + self.assertTrue(app_status.reconciled_at) + + self.assertIsNotNone(app_status.source) + self.assertTrue(app_status.source.path) + self.assertTrue(app_status.source.revision) + self.assertTrue(app_status.source.repourl) + + self.assertIsNotNone(app_status.sync_ressources_status) + for resource_sync_status in app_status.sync_ressources_status: + self.assertTrue(resource_sync_status.name) + self.assertTrue(resource_sync_status.kind) + self.assertTrue(resource_sync_status.status) + self.assertTrue(resource_sync_status.message) + + def assert_application(self, app_dict: dict, app: Application): + app_dict_c = copy.deepcopy(app_dict) + self.assertTrue(app_dict_c['application_name'] in app.application_name) + del app_dict_c['application_name'] + self.assert_object(app_dict_c, app) + + def test_applications(self): + # create with repo url + app_dict = self._get_application_dict() + response = self.ai_core_v2_client.applications.create(**app_dict) + self.assertTrue(app_dict['application_name'] in response.id) + self.assertIsNotNone(response.message) + + # get + app = self.ai_core_v2_client.applications.get(application_name=app_dict['application_name']) + self.assert_application(app_dict, app) + + # get_status + app_status = self.ai_core_v2_client.applications.get_status(application_name=app_dict['application_name']) + self.assert_app_status(app_status) + + # force sync git repo + response = self.ai_core_v2_client.applications.refresh(application_name=app_dict['application_name']) + self.assertTrue(app_dict['application_name'] in response.id) + print(response.message) + for t in ['refresh', 'scheduled']: + self.assertIn(t, response.message) + + # create with repo name + app_dict_2 = self._get_application_dict() + app_dict_2['repository_name'] = self.repo_dict['name'] + del app_dict_2['repository_url'] + response = self.ai_core_v2_client.applications.create(**app_dict_2) + self.assertTrue(app_dict_2['application_name'] in response.id) + self.assertIsNotNone(response.message) + + # get + app_2 = self.ai_core_v2_client.applications.get(application_name=app_dict_2['application_name']) + app_dict_2_url = app_dict_2.copy() + app_dict_2_url['repository_url'] = self.repo_dict['url'] + del app_dict_2_url['repository_name'] + self.assert_application(app_dict_2_url, app_2) + + # query + apps_qr = self.ai_core_v2_client.applications.query() + self.assertTrue(apps_qr.count >= 2) + self.assert_app_dict_in_app_objects(app_dict, apps_qr.resources) + self.assert_app_dict_in_app_objects(app_dict_2_url, apps_qr.resources) + + # modify + app_patch_dict = self._get_application_dict() + app_patch_dict['application_name'] = app_dict_2['application_name'] + app_patch_dict['revision'] = 'test-patch-revision' + app_patch_dict['path'] = 'test-patch-path' + response = self.ai_core_v2_client.applications.modify(**app_patch_dict) + self.assertTrue(app_patch_dict['application_name'] in response.id) + self.assertIsNotNone(response.message) + + # get modified app + app_patched = self.ai_core_v2_client.applications.get(app_dict_2['application_name']) + self.assert_application(app_patch_dict, app_patched) + + # delete + response = self.ai_core_v2_client.applications.delete(application_name=app_dict['application_name']) + self.assertTrue(app_dict['application_name'] in response.id) + self.assertIsNotNone(response.message) + self.ai_core_v2_client.applications.delete(application_name=app_dict_2['application_name']) diff --git a/packages/core/integration_tests/test_e2e_docker_registry_secrets.py b/packages/core/integration_tests/test_e2e_docker_registry_secrets.py new file mode 100644 index 0000000..df2cdc6 --- /dev/null +++ b/packages/core/integration_tests/test_e2e_docker_registry_secrets.py @@ -0,0 +1,53 @@ +from typing import List + +from . import get_random_string +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.docker_registry_secret import DockerRegistrySecret + + +class TestE2EDockerRegistrySecrets(AICoreV2ClientE2ETestBase): + + @staticmethod + def _get_docker_registry_secret_data(): + return { + 'name': f'test-docker-registry-secret-{get_random_string()}', + 'data': { + ".dockerconfigjson": "{\"auths\": {\"test_docker_registry_url\": {\"username\": \"test_docker_username\", \"password\": \"test_docker_password\"}}}" + } + } + + @staticmethod + def _is_name_in_secrets(name: str, secrets: List[DockerRegistrySecret]): + for drs in secrets: + if name == drs.name: + return True + return False + + def test_docker_registry_secrets(self): + drs_dict = self._get_docker_registry_secret_data() + response = self.ai_core_v2_client.docker_registry_secrets.create(name=drs_dict['name'], data=drs_dict['data']) + self.assertIsNotNone(response.message) + + drs = self.ai_core_v2_client.docker_registry_secrets.get(name=drs_dict['name']) + self.assertEqual(drs_dict['name'], drs.name) + + dr_secrets = self.ai_core_v2_client.docker_registry_secrets.query() + self.assertIsNotNone(dr_secrets.resources) + self.assertTrue(dr_secrets.count >= 1) + self.assertTrue(self._is_name_in_secrets(drs_dict['name'], dr_secrets.resources)) + n = dr_secrets.count + + dr_secrets_top = self.ai_core_v2_client.docker_registry_secrets.query(top=2) + self.assertTrue(1 <= len(dr_secrets_top.resources) <= 2) + + dr_secrets_skip = self.ai_core_v2_client.docker_registry_secrets.query(skip=1) + self.assertEqual(n-1, len(dr_secrets_skip.resources)) + + patch_data = {".dockerconfigjson": "{\"auths\": {\"test_docker_registry_url\": {\"username\": \"test_docker_username2\", \"password\": \"test_docker_password\"}}}"} + response = self.ai_core_v2_client.docker_registry_secrets.modify(name=drs_dict['name'], data=patch_data) + self.assertEqual(drs_dict['name'], response.id) + self.assertIsNotNone(response.message) + + response = self.ai_core_v2_client.docker_registry_secrets.delete(name=drs_dict['name']) + self.assertEqual(drs_dict['name'], response.id) + self.assertIsNotNone(response.message) diff --git a/packages/core/integration_tests/test_e2e_kpis.py b/packages/core/integration_tests/test_e2e_kpis.py new file mode 100644 index 0000000..60caf4d --- /dev/null +++ b/packages/core/integration_tests/test_e2e_kpis.py @@ -0,0 +1,14 @@ +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.kpi import Kpi + + +class TestE2EKpis(AICoreV2ClientE2ETestBase): + + def _validate_data(self, kpis_data: dict): + self.assertIsInstance(kpis_data, Kpi) + for row in kpis_data.rows: + self.assertEqual(len(kpis_data.header), len(row)) + def test_kpis(self): + kpis_data = self.ai_core_v2_client.kpis.query() + #validating created data + self._validate_data(kpis_data) diff --git a/packages/core/integration_tests/test_e2e_metrics.py b/packages/core/integration_tests/test_e2e_metrics.py new file mode 100644 index 0000000..4af10dc --- /dev/null +++ b/packages/core/integration_tests/test_e2e_metrics.py @@ -0,0 +1,254 @@ +import os +from typing import List +from datetime import datetime +from unittest.mock import patch + +from ai_api_client_sdk.exception import AIAPIInvalidRequestException +from ai_api_client_sdk.helpers.datetime_parser import DATETIME_FORMAT +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.input_artifact import InputArtifact +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.label import Label +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_tag import MetricTag +from ai_api_client_sdk.models.parameter import Parameter +from ai_api_client_sdk.models.parameter_binding import ParameterBinding + +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase + + +class TestE2EMetrics(AICoreV2ClientE2ETestBase): + + def setUp(self): + super().setUp() + configuration = self.get_configuration() + res = self.ai_core_v2_client.execution.create(configuration_id=configuration.id) + self.execution_id = res.id + + def create_configuration_dict(self, executable_id: str, parameters: List[Parameter] = None, + input_artifacts: List[InputArtifact] = None): + configuration_dict = { + "name": "configuration_name", + "executable_id": executable_id, + "scenario_id": self.test_scenario_id + } + if parameters: + configuration_dict['parameter_bindings'] = [ + ParameterBinding(key=p.name, value=f'Test {p.name} value') for p in parameters + ] + if input_artifacts: + artifact = self.get_an_artifact() + configuration_dict['input_artifact_bindings'] = [ + InputArtifactBinding(key=ia.name, artifact_id=artifact.id) for ia in input_artifacts + ] + return configuration_dict + + def create_artifact_dict(self, i=1, labels=None, kind: Artifact.Kind = Artifact.Kind.MODEL): + if not labels: + labels = [ + Label(**{ + "key": "ext.ai.sap.com/s4hana-version", + "value": "string" + }) + ] + return { + 'name': f'Test Artifact {i}', + 'kind': kind, + 'labels': labels, + 'url': 'gs://kfserving-samples/models/tensorflow/flowers', + 'scenario_id': self.test_scenario_id, + 'description': f'Test Artifact {i} description' + } + + def get_an_artifact(self): + res = self.ai_core_v2_client.artifact.query(scenario_id=self.test_scenario_id) + if res.count > 0: + return res.resources[0] + artifact_dict = self.create_artifact_dict() + res = self.ai_core_v2_client.artifact.create(**artifact_dict) + return self.ai_core_v2_client.artifact.get(artifact_id=res.id) + + def get_executable(self): + res = self.ai_core_v2_client.executable.query(scenario_id=self.test_scenario_id) + self.assertEqual(res.count, len(res.resources)) + self.assertTrue(res.count > 0) + executables = res.resources + for e in executables: + if not e.deployable: + return e + + def get_configuration(self): + executable = self.get_executable() + configuration_dict = self.create_configuration_dict(executable_id=executable.id, + parameters=executable.parameters, + input_artifacts=executable.input_artifacts) + res = self.ai_core_v2_client.configuration.create(**configuration_dict) + return self.ai_core_v2_client.configuration.get(configuration_id=res.id) + + @staticmethod + def __patch_metrics_body(execution_id): + patch_mb = { + "executionId": execution_id, + "metrics": [ + { + "name": "Test Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Test Artifact Group", + "value": "RFC-1" + } + ], + "customInfo": [ + { + "name": "Test Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + return patch_mb + + def test_patch_metrics(self): + metrics_patch_data = self.__patch_metrics_body(self.execution_id) + body = {'execution_id': metrics_patch_data.get('executionId'), + 'metrics': [Metric.from_dict(md) for md in metrics_patch_data['metrics']], + 'tags': [MetricTag.from_dict(mtd) for mtd in metrics_patch_data['tags']], + 'custom_info': [MetricCustomInfo.from_dict(mcid) for mcid in metrics_patch_data['customInfo']]} + self.tracking_client.modify(**body) + response = self.tracking_client.query(execution_ids=[self.execution_id]) + metric_resource = response.resources[response.count - 1] + metric_dict = {} + metric_dict['execution_id'] = metric_resource.execution_id + self.assertIsNotNone(metric_resource.metrics) + self.assertIsNotNone(metric_resource.tags) + self.assertIsNotNone(metric_resource.custom_info) + metric_dict['metrics'] = [metric.__dict__ for metric in metric_resource.metrics] + metric_dict['tags'] = [tag.__dict__ for tag in metric_resource.tags] + metric_dict['custom_info'] = [c_info.__dict__ for c_info in metric_resource.custom_info] + self.assertEqual(metric_dict['execution_id'], metrics_patch_data['executionId']) + self.assertEqual(metric_dict['metrics'], metrics_patch_data['metrics']) + self.assertTrue(all(t in metric_dict['tags'] for t in metrics_patch_data['tags'])) + self.assertEqual(metric_dict['custom_info'], metrics_patch_data['customInfo']) + with self.assertRaises(AIAPIInvalidRequestException): + self.tracking_client.modify(execution_id='') + + # test_smoke_get_resource_group_still_working_with_metrics_within_aicore aims to + # test if functionality that is not related to metrics still works when the sdk + # is used within aicore. This is done by faking aicore internal usage by setting + # environment variables. No actual calls to the metrics service are done. Since + # the integration tests are not running within aicore it is not possible to test + # if metrics related calls are still working within aicore. + # In summary: This test implicitly checks if functionality unrelated to metrics + # is not broken by metrics internals. + @patch.dict( + os.environ, + { + "AICORE_EXECUTION_ID": "test-execution-id", + "AICORE_TRACKING_ENDPOINT": "test-tracking-endpoint", + "AI-MAIN-TENANT": "test-main-tenant", + "AI-RESOURCE-GROUP": "test-resource-group", + }, + ) + def test_smoke_get_resource_group_still_working_with_metrics_within_aicore(self): + self.ai_core_v2_client.scenario.get(self.test_scenario_id).id + self.assertEqual( + self.ai_core_v2_client.scenario.get(self.test_scenario_id).id, + self.test_scenario_id, + ) + + def test_log_metrics(self): + metrics_data = { + "execution_id": self.execution_id, + "metrics": [ + Metric( + name="Training Loss", + value=float(86.99), + timestamp=datetime.now().utcnow(), + step=1, # denotes epoch 1 + labels=[] + ), + Metric( + name="Training Loss", + value=float(89.00), + timestamp=datetime.now().utcnow(), + step=2, # denotes epoch 2 + labels=[] + ), + ], + "artifact_name": "test artifact" + } + self.tracking_client.log_metrics(**metrics_data) + response = self.tracking_client.query(execution_ids=[self.execution_id]) + metric_resource = response.resources[response.count - 1] + self.assertIsNotNone(metric_resource.metrics) + self.assertEqual(len(metric_resource.metrics), 2) + self.assertEqual(metric_resource.execution_id, self.execution_id) + for index in range(1, 2): + self.assertEqual(metric_resource.metrics[index].name, metrics_data['metrics'][index].name) + self.assertEqual(metric_resource.metrics[index].step, metrics_data['metrics'][index].step) + self.assertEqual(metric_resource.metrics[index].value, metrics_data['metrics'][index].value) + self.assertEqual(metric_resource.metrics[index].timestamp.strftime(DATETIME_FORMAT), + metrics_data['metrics'][index].timestamp.strftime(DATETIME_FORMAT)) + self.assertEqual(metric_resource.metrics[index].labels[0].name, + 'metrics.ai.sap.com/Artifact.name') + self.assertEqual(metric_resource.metrics[index].labels[0].value, + 'test artifact') + + def test_set_tags(self): + tags_data = { + "execution_id": self.execution_id, + "tags": [ + # list + MetricTag(name="Our Team Tag", value="Tutorial Team"), + MetricTag(name="Stage", value="Development") + ] + } + self.tracking_client.set_tags(**tags_data) + response = self.tracking_client.query(execution_ids=[self.execution_id]) + execution_resource = response.resources[response.count - 1] + self.assertGreaterEqual(len(execution_resource.tags), 2) + self.assertEqual(execution_resource.execution_id, self.execution_id) + self.assertTrue(all(t in execution_resource.tags for t in tags_data['tags'])) + + def test_set_custom_info(self): + custom_info_data = { + "execution_id": self.execution_id, + "custom_info": [ + # list + MetricCustomInfo( + name="My Classification Report", + # you may convert anything to string and store it + value=str(''' + { + "Cats": { + "Precision": 100, + "Recall": 100 + }, + "Dogs": { + "Precision": 200, + "Recall": 200 + } + } + ''' + ) + ), + ] + } + self.tracking_client.set_custom_info(**custom_info_data) + response = self.tracking_client.query(execution_ids=[self.execution_id]) + execution_resource = response.resources[response.count - 1] + self.assertEqual(len(execution_resource.custom_info), 1) + self.assertEqual(execution_resource.execution_id, self.execution_id) + for index in range(1): + self.assertEqual(execution_resource.custom_info[index].name, custom_info_data['custom_info'][index].name) diff --git a/packages/core/integration_tests/test_e2e_object_store_secrets.py b/packages/core/integration_tests/test_e2e_object_store_secrets.py new file mode 100644 index 0000000..fbe00b3 --- /dev/null +++ b/packages/core/integration_tests/test_e2e_object_store_secrets.py @@ -0,0 +1,98 @@ +from typing import List + +from . import get_random_string +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.object_store_secret import ObjectStoreSecret + + +class TestE2EObjectStoreSecrets(AICoreV2ClientE2ETestBase): + + @staticmethod + def _get_object_store_secret_data(): + return { + 'name': f'test-{get_random_string()}', + 'type': 'S3', + 'bucket': f'{get_random_string()}-bucket', + 'endpoint': f's3-{get_random_string()}.com', + 'pathPrefix': get_random_string(), + 'verifyssl': '0', + 'usehttps': '1', + 'region': 'eu-central-1', + 'data': { + "AWS_ACCESS_KEY_ID": get_random_string(), + "AWS_SECRET_ACCESS_KEY": get_random_string() + } + } + + @staticmethod + def _is_name_in_secrets(name: str, secrets: List[ObjectStoreSecret]): + for oss in secrets: + if name == oss.name: + return True + return False + + def _validate_data(self, oss_dict: dict, response_oss: ObjectStoreSecret): + STORAGE_PREFIX = 'storage.ai.sap.com/' + SERVING_KUBEFLOW_PREFIX = 'serving.kubeflow.org/' + self.assertEqual(oss_dict['name'], response_oss.name) + self.assertEqual(oss_dict['type'], response_oss.metadata[f'{STORAGE_PREFIX}type']) + self.assertEqual(oss_dict['bucket'], response_oss.metadata[f'{STORAGE_PREFIX}bucket']) + self.assertEqual(oss_dict['endpoint'], response_oss.metadata[f'{STORAGE_PREFIX}endpoint']) + self.assertEqual(oss_dict['region'], response_oss.metadata[f'{STORAGE_PREFIX}region']) + self.assertEqual(oss_dict['pathPrefix'], response_oss.metadata[f'{STORAGE_PREFIX}pathPrefix']) + self.assertEqual(oss_dict['endpoint'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-endpoint']) + self.assertEqual(oss_dict['region'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-region']) + self.assertEqual(oss_dict['usehttps'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-usehttps']) + self.assertEqual(oss_dict['verifyssl'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-verifyssl']) + + def test_object_store_secrets(self): + STORAGE_PREFIX = 'storage.ai.sap.com/' + SERVING_KUBEFLOW_PREFIX = 'serving.kubeflow.org/' + oss_dict = self._get_object_store_secret_data() + response = self.ai_core_v2_client.object_store_secrets.create(name=oss_dict['name'], + type=oss_dict['type'], + bucket=oss_dict['bucket'], + endpoint=oss_dict['endpoint'], + path_prefix=oss_dict['pathPrefix'], + verifyssl=oss_dict['verifyssl'], + usehttps=oss_dict['usehttps'], + region=oss_dict['region'], + data=oss_dict['data']) + self.assertIsNotNone(response.message) + + oss = self.ai_core_v2_client.object_store_secrets.get(name=oss_dict['name']) + #validating created data + self._validate_data(oss_dict, oss) + + os_secrets = self.ai_core_v2_client.object_store_secrets.query() + self.assertIsNotNone(os_secrets.resources) + self.assertTrue(os_secrets.count >= 1) + self.assertTrue(self._is_name_in_secrets(oss_dict['name'], os_secrets.resources)) + n = os_secrets.count + + os_secrets_top = self.ai_core_v2_client.object_store_secrets.query(top=2) + self.assertTrue(1 <= len(os_secrets_top.resources) <= 2) + + os_secrets_skip = self.ai_core_v2_client.object_store_secrets.query(skip=1) + self.assertEqual(n-1, len(os_secrets_skip.resources)) + + patch_data = {"AWS_ACCESS_KEY_ID": get_random_string(), "AWS_SECRET_ACCESS_KEY": get_random_string()} + response = self.ai_core_v2_client.object_store_secrets.modify(name=oss_dict['name'], + type=oss_dict['type'], + bucket=oss_dict['bucket'], + endpoint=oss_dict['endpoint'], + path_prefix=oss_dict['pathPrefix'], + region=oss_dict['region'], + data=patch_data) + + self.assertEqual(oss_dict['name'], response.id) + self.assertIsNotNone(response.message) + + oss = self.ai_core_v2_client.object_store_secrets.get(name=oss_dict['name']) + + # validating modified data + self._validate_data(oss_dict, oss) + + response = self.ai_core_v2_client.object_store_secrets.delete(name=oss_dict['name']) + self.assertEqual(oss_dict['name'], response.id) + self.assertIsNotNone(response.message) diff --git a/packages/core/integration_tests/test_e2e_repositories.py b/packages/core/integration_tests/test_e2e_repositories.py new file mode 100644 index 0000000..be377ec --- /dev/null +++ b/packages/core/integration_tests/test_e2e_repositories.py @@ -0,0 +1,48 @@ +from typing import List + +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.repository import Repository + + +class TestE2ERepositories(AICoreV2ClientE2ETestBase): + + def assert_repo(self, repo_dict: dict, repo: Repository): + self.assertEqual(repo_dict['name'], repo.name) + self.assertEqual(repo_dict['url'], repo.url) + self.assertIsNotNone(repo.status) + + def assert_repo_dict_in_repo_objects(self, repo_dict: dict, repos: List[Repository]): + for repo in repos: + if repo.name == repo_dict['name']: + self.assert_repo(repo_dict, repo) + return + + def test_repositories(self): + # create + repo_dict = self._get_repo_dict() + response = self.ai_core_v2_client.repositories.create(**repo_dict) + self.assertIsNotNone(response.message) + + # get + repo = self.ai_core_v2_client.repositories.get(name=repo_dict['name']) + self.assert_repo(repo_dict, repo) + + # query + repo_qr = self.ai_core_v2_client.repositories.query() + self.assertTrue(repo_qr.count >= 1) + self.assert_repo_dict_in_repo_objects(repo_dict, repo_qr.resources) + + # modify + repo_patch_dict = { + 'name': repo_dict['name'], + 'username': 'test-patch-username', + 'password': 'test-patch-password' + } + response = self.ai_core_v2_client.repositories.modify(**repo_patch_dict) + self.assertEqual(repo_dict['name'], response.id) + self.assertIsNotNone(response.message) + + # delete + response = self.ai_core_v2_client.repositories.delete(name=repo_dict['name']) + self.assertEqual(repo_dict['name'], response.id) + self.assertIsNotNone(response.message) diff --git a/packages/core/integration_tests/test_e2e_secrets.py b/packages/core/integration_tests/test_e2e_secrets.py new file mode 100644 index 0000000..7b870ec --- /dev/null +++ b/packages/core/integration_tests/test_e2e_secrets.py @@ -0,0 +1,59 @@ +from typing import List + +from . import get_random_string +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.models.secret import Secret + + +class TestE2ESecrets(AICoreV2ClientE2ETestBase): + + @staticmethod + def _get_secret_data(): + return { + 'name': f'test-{get_random_string()}', + 'data': { + "prop1": get_random_string(), + "prop2": get_random_string() + } + } + + @staticmethod + def _is_name_in_secrets(name: str, secrets: List[Secret]): + for secret in secrets: + if name == secret.name: + return True + return False + + def test_secrets(self): + secret_dict = self._get_secret_data() + response = self.ai_core_v2_client.secrets.create(name=secret_dict['name'], + data=secret_dict['data'], + ai_tenant_scope=False) + self.assertIsNotNone(response.message) + + secrets = self.ai_core_v2_client.secrets.query(ai_tenant_scope=False) + self.assertIsNotNone(secrets.resources) + self.assertTrue(secrets.count >= 1) + self.assertTrue(self._is_name_in_secrets(secret_dict['name'], secrets.resources)) + n = secrets.count + + secrets_top = self.ai_core_v2_client.secrets.query(top=2, ai_tenant_scope=False) + self.assertTrue(1 <= len(secrets_top.resources) <= 2) + + secrets_skip = self.ai_core_v2_client.secrets.query(skip=1, ai_tenant_scope=False) + self.assertEqual(n-1, len(secrets_skip.resources)) + + patch_data = {"prop1": get_random_string(), "prop2": get_random_string()} + response = self.ai_core_v2_client.secrets.modify(name=secret_dict['name'], + data=patch_data, + ai_tenant_scope=False) + + self.assertEqual("The secret has been modified", response.message) + + response = self.ai_core_v2_client.secrets.delete(name=secret_dict['name'], + ai_tenant_scope=False) + self.assertEqual("Secret has been deleted", response.message) + + secrets = self.ai_core_v2_client.secrets.query(ai_tenant_scope=False) + deleted_secret = [secret for secret in secrets.resources if secret.name == secret_dict['name']] + self.assertEqual(len(deleted_secret), 0) diff --git a/packages/core/integration_tests/test_e2e_x509.py b/packages/core/integration_tests/test_e2e_x509.py new file mode 100644 index 0000000..bfe1372 --- /dev/null +++ b/packages/core/integration_tests/test_e2e_x509.py @@ -0,0 +1,103 @@ +import json +import os +import tempfile +from typing import List +from unittest.mock import patch + +from . import (BASE_URL, CLIENT_ID, RESOURCE_GROUP_ID, X509_CERT_URL, X509_CERT_FILE_PATH, X509_KEY_FILE_PATH, + X509_CERT_STR, X509_KEY_STR) +from . import write_x509_credentials_into_files, remove_x509_credentials +from .ai_core_v2_client_e2e_test_base import AICoreV2ClientE2ETestBase +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.helpers.constants import (AI_CORE_PREFIX, HOME_PATH_ENV_VAR, VCAP_AICORE_SERVICE_NAME, + VCAP_SERVICES_ENV_VAR) +from ai_core_sdk.models import Scenario + + +VCAP_SERVICE_X509_DICT = { + VCAP_AICORE_SERVICE_NAME: [{ + 'label': VCAP_AICORE_SERVICE_NAME, + 'name': f'{VCAP_AICORE_SERVICE_NAME}-instance', + 'instance_guid': '53ad5b47-a49a-4fec-9f0b-cd921c00b828', + 'credentials': { + 'serviceurls': { + 'AI_API_URL': BASE_URL[:-3] + }, + 'certurl': X509_CERT_URL, + 'clientid': CLIENT_ID, + 'key': X509_KEY_STR.replace('\n', '\\n'), + 'certificate': X509_CERT_STR.replace('\n', '\\n') + } + }] +} +VCAP_SERVICE_X509_ENV_VALUE = json.dumps(VCAP_SERVICE_X509_DICT, indent=4) + + +class TestE2EX509(AICoreV2ClientE2ETestBase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # write_x509_credentials_into_files() + cls.valid_x509_config = { + f'{AI_CORE_PREFIX}_CLIENT_ID': CLIENT_ID, + f'{AI_CORE_PREFIX}_CERT_URL': X509_CERT_URL, + f'{AI_CORE_PREFIX}_CERT_FILE_PATH': X509_CERT_FILE_PATH, + f'{AI_CORE_PREFIX}_KEY_FILE_PATH': X509_KEY_FILE_PATH, + f'{AI_CORE_PREFIX}_RESOURCE_GROUP': RESOURCE_GROUP_ID, + f'{AI_CORE_PREFIX}_BASE_URL': BASE_URL + } + + @classmethod + def tearDownClass(cls) -> None: + # Uncomment the following line and the one in setUpClass to run integration_tests via IDE + # remove_x509_credentials() + super().tearDownClass() + + @staticmethod + def _get_scenario_from_scenarios(scenarios: List[Scenario], scenario_id: str): + for s in scenarios: + if s.id == scenario_id: + return s + return None + + def _query_and_assert_scenarios(self, client: AICoreV2Client): + response = client.scenario.query() + scenarios = response.resources + self.assertEqual(response.count, len(scenarios)) + queried_scenario = self._get_scenario_from_scenarios(scenarios, self.test_scenario_id) + scenario = client.scenario.get(scenario_id=self.test_scenario_id) + self.assertEqual(queried_scenario, scenario) + self.assertIsNotNone(scenario.id) + self.assertIsNotNone(scenario.name) + + @patch.dict(os.environ, {VCAP_SERVICES_ENV_VAR: VCAP_SERVICE_X509_ENV_VALUE, 'AICORE_RESOURCE_GROUP': RESOURCE_GROUP_ID}) + def test_x509_from_vcap(self): + client = AICoreV2Client.from_env() + self._query_and_assert_scenarios(client) + + def test_x509_from_profile(self): + with tempfile.TemporaryDirectory() as temp_dir: + profile = 'test' + default_config_path = os.path.join(temp_dir, 'config.json') + profile_config_path = os.path.join(temp_dir, f'config_{profile}.json') + with open(default_config_path, 'w') as f: + json.dump({}, f) + with open(profile_config_path, 'w') as f: + json.dump(self.valid_x509_config, f) + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: str(temp_dir)}): + client = AICoreV2Client.from_env(profile_name=profile) + self._query_and_assert_scenarios(client) + + + def test_x509_file_path(self): + client = AICoreV2Client(base_url=BASE_URL, auth_url=X509_CERT_URL, client_id=CLIENT_ID, + cert_file_path=X509_CERT_FILE_PATH, key_file_path=X509_KEY_FILE_PATH, + resource_group=RESOURCE_GROUP_ID) + self._query_and_assert_scenarios(client) + + def test_x509_str(self): + client = AICoreV2Client(base_url=BASE_URL, auth_url=X509_CERT_URL, client_id=CLIENT_ID, cert_str=X509_CERT_STR, + key_str=X509_KEY_STR, resource_group=RESOURCE_GROUP_ID) + self._query_and_assert_scenarios(client) \ No newline at end of file diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml new file mode 100644 index 0000000..bc7784a --- /dev/null +++ b/packages/core/pyproject.toml @@ -0,0 +1,67 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "sap-ai-sdk-core" +version = "3.3.1" +description = "SAP Cloud SDK for AI (Python): Core SDK" +readme = "PYPIDESCRIPTION.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [{ name = "SAP SE" }] +keywords = ["SAP AI Core", "SAP AI Core API"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.9" +dependencies = ["sap-ai-sdk-base~=3.4", "click~=8.3"] + +[dependency-groups] +dev = [ + "pytest==9.0.3", + "pytest-cov==7.1.0", + "pylint==4.0.5", + "pyhamcrest==2.1.0", + "pytest-dotenv>=0.5.2", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +norecursedirs = ["integration_tests"] + +[project.scripts] +aicore = "ai_core_sdk.cli:cli" + +[project.urls] +Homepage = "https://www.sap.com/" +Download = "https://pypi.python.org/pypi/sap-ai-sdk-core" + +[tool.uv.sources] +sap-ai-sdk-base = { workspace = true } + +[tool.setuptools.packages.find] +exclude = ["*test*"] + +[tool.commitizen] +name = "cz_customize" +tag_format = "core-v${version}" +ignored_tag_formats = ["*-v${version}"] +version_provider = "pep621" +changelog_file = "RELEASE_NOTES.md" + +[tool.commitizen.customize] +bump_pattern = '^(feat|fix)\(core\)' +changelog_pattern = '^(feat|fix)\(core\)' diff --git a/packages/core/sonar-project.properties b/packages/core/sonar-project.properties new file mode 100644 index 0000000..fb4614a --- /dev/null +++ b/packages/core/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=ai-core-sdk +sonar.projectName=ai-core-sdk +sonar.projectVersion=3.3.0 +sonar.sources=./ai_core_sdk +sonar.exclusions=scripts/**,tests/**,integration_tests/**, ai_core_sdk/helpers/constants.py +sonar.dynamicAnalysis=reuseReports +sonar.core.codeCoveragePlugin=cobertura +sonar.python.coverage.reportPaths=**/coverage.xml +sonar.python.xunit.reportPath=**/unit_tests.xml +sonar.python.pylint.reportPath=**/pylint.log +sonar.qualitygate.wait=true diff --git a/packages/core/tests/__init__.py b/packages/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/tests/ai_core_client/__init__.py b/packages/core/tests/ai_core_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/tests/ai_core_client/test_ai_core_v2_client.py b/packages/core/tests/ai_core_client/test_ai_core_v2_client.py new file mode 100644 index 0000000..6c49260 --- /dev/null +++ b/packages/core/tests/ai_core_client/test_ai_core_v2_client.py @@ -0,0 +1,336 @@ +import os +import json +import tempfile +import unittest +from unittest import TestCase +from unittest.mock import MagicMock, patch +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.helpers.constants import (AI_CORE_PREFIX, HOME_PATH_ENV_VAR, VCAP_SERVICES_ENV_VAR, + VCAP_AICORE_SERVICE_NAME) +from ai_core_sdk.exception import AIAPIAuthenticatorException +from ai_core_sdk.resource_clients.internal_rest_client import InternalRestClient +from .test_credentials import VCAP_SERVICE_X509_DICT, VCAP_SERVICE_X509_ENV_VALUE + +# unit tests for AI Core V2 Client + +params = ('base_url', 'auth_url', 'resource_group', 'client_id', 'client_secret') + + +class TestAICoreV2Client(TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.base_url = 'test_base_url/v2' + cls.auth_url = 'test_auth_url/oauth/token' + cls.client_id = 'test_client_id' + cls.client_secret = 'test_client_secret' + cls.cert_str = 'test_cert_str' + cls.key_str = 'test_key_str' + cls.cert_file_path = 'test_cert_file_path' + cls.key_file_path = 'test_key_file_path' + + def test_no_secret_no_cert_raises_exception(self): + with self.assertRaises(AIAPIAuthenticatorException) as cm: + c = AICoreV2Client(base_url=self.base_url, auth_url=self.auth_url) + self.assertTrue('client_id' in cm.exception.error_message) + + @patch('ai_core_sdk.ai_core_v2_client.AIAPIV2Client') + def test_happy_path_client_secret(self, ai_api_v2_client_mock): + ai_api_v2_client_mock._create_token_creator_if_does_not_exist = MagicMock() + c = AICoreV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + client_secret=self.client_secret) + ai_api_v2_client_mock._create_token_creator_if_does_not_exist.assert_called_once_with( + token_creator=None, auth_url=self.auth_url, client_id=self.client_id, client_secret=self.client_secret, + cert_str=None, key_str=None, cert_file_path=None, key_file_path=None) + self.assertIsNotNone(c.rest_client) + + @patch('ai_core_sdk.ai_core_v2_client.AIAPIV2Client') + def test_happy_path_x509_file_path(self, ai_api_v2_client_mock): + ai_api_v2_client_mock._create_token_creator_if_does_not_exist = MagicMock() + c = AICoreV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + ai_api_v2_client_mock._create_token_creator_if_does_not_exist.assert_called_once_with( + token_creator=None, auth_url=self.auth_url, client_id=self.client_id, client_secret=None, cert_str=None, + key_str=None, cert_file_path=self.cert_file_path, key_file_path=self.key_file_path) + self.assertIsNotNone(c.rest_client) + + @patch('ai_core_sdk.ai_core_v2_client.AIAPIV2Client') + def test_happy_path_x509_str(self, ai_api_v2_client_mock): + ai_api_v2_client_mock._create_token_creator_if_does_not_exist = MagicMock() + c = AICoreV2Client(base_url=self.base_url, auth_url=self.auth_url, client_id=self.client_id, + cert_str=self.cert_str, key_str=self.key_str) + ai_api_v2_client_mock._create_token_creator_if_does_not_exist.assert_called_once_with( + token_creator=None, auth_url=self.auth_url, client_id=self.client_id, client_secret=None, + cert_str=self.cert_str, key_str=self.key_str, cert_file_path=None, key_file_path=None) + self.assertIsNotNone(c.rest_client) + + @property + def full_params(self): + full_params = dict( + base_url = 'https://ai-api.com/v2', + auth_url = 'https://auth.com/oauth/token', + client_id = 'client_id', + client_secret = 'client_secret', + resource_group = 'test_resource_group', + read_timeout = 1, + connect_timeout = 2, + num_request_retries = 3, + ) + return full_params + + @property + def full_params_no_suffix(self): + params_with_suffix = self.full_params + params_with_suffix['base_url'] = 'https://ai-api.com/' + params_with_suffix['auth_url'] = 'https://auth.com/' + return params_with_suffix + + def test_regular_rest_client_used_outside_aicore(self): + client = AICoreV2Client(**self.full_params) + self.assertNotIsInstance(client.metrics.rest_client, InternalRestClient) + + def test_client_type_header_set_correctly(self): + test_client_type = "Test Client Type" + test_params = self.full_params.copy() + test_params['client_type'] = test_client_type + client = AICoreV2Client(**test_params) + client_headers = client.rest_client.headers + self.assertEqual(client_headers['AI-Client-Type'], test_client_type) + + def test_client_type_header_defaults_correctly(self): + test_params = self.full_params.copy() + test_params.pop('client_type', None) + client = AICoreV2Client(**test_params) + client_headers = client.rest_client.headers + self.assertEqual(client_headers['AI-Client-Type'], "AI Core Python SDK") + + @patch.dict( + os.environ, + { + "AICORE_EXECUTION_ID": "test-execution-id", + "AICORE_TRACKING_ENDPOINT": "test-tracking-endpoint", + "AI-MAIN-TENANT": "test-main-tenant", + "AI-RESOURCE-GROUP": "test_resource_group", + }, + ) + def test_internal_rest_client_used_within_aicore(self): + client = AICoreV2Client(**self.full_params) + self.assertEqual(client.rest_client.headers.get(client.rest_client.client_type_header), "AI Core Python SDK") + + self.assertIsInstance(client.metrics.rest_client, InternalRestClient) + + # check if attributes adjusted by InternalRestClient are properly set + self.assertEqual( + client.metrics.rest_client.base_url, "test-tracking-endpoint/api/v1" + ) + self.assertEqual( + client.metrics.rest_client.resource_group_header, "AI-Resource-Group" + ) + self.assertEqual(client.metrics.rest_client.get_token(), "") + + # check if other parameters are properly set + partial_params = { + x: self.full_params[x] + for x in self.full_params + if x + not in { + "base_url", + "resource_group", + "token_creator", + # also excluding parameters transformed further + "auth_url", + "client_id", + "client_secret", + } + } + for param_key in partial_params: + self.assertEqual( + getattr(client.metrics.rest_client, param_key), + partial_params[param_key], + ) + + def get_x509_file_path_dict(self): + return { + f'{AI_CORE_PREFIX}_BASE_URL': self.base_url, + f'{AI_CORE_PREFIX}_AUTH_URL': self.auth_url, + f'{AI_CORE_PREFIX}_CLIENT_ID': self.client_id, + f'{AI_CORE_PREFIX}_CERT_FILE_PATH': self.cert_file_path, + f'{AI_CORE_PREFIX}_KEY_FILE_PATH': self.key_file_path + } + + def get_x509_str_dict(self): + return { + f'{AI_CORE_PREFIX}_BASE_URL': self.base_url, + f'{AI_CORE_PREFIX}_AUTH_URL': self.auth_url, + f'{AI_CORE_PREFIX}_CLIENT_ID': self.client_id, + f'{AI_CORE_PREFIX}_CERT_STR': self.cert_str, + f'{AI_CORE_PREFIX}_KEY_STR': self.key_str + } + + def test_x509_str_from_env(self): + init_mock = MagicMock(return_value=None) + aicv2c_init = AICoreV2Client.__init__ + AICoreV2Client.__init__ = init_mock + mock_env = self.get_x509_str_dict() + + with patch.dict(os.environ, mock_env): + AICoreV2Client.from_env() + AICoreV2Client.__init__.assert_called_once_with(base_url=self.base_url, auth_url=self.auth_url, + client_id=self.client_id, + cert_str=self.cert_str, + key_str=self.key_str) + + AICoreV2Client.__init__ = aicv2c_init + + def test_x509_file_path_from_config(self): + init_mock = MagicMock(return_value=None) + aicv2c_init = AICoreV2Client.__init__ + AICoreV2Client.__init__ = init_mock + + config = self.get_x509_file_path_dict() + for k, v in config.items(): + config[k] = f'cfg_{v}' + + with tempfile.TemporaryDirectory() as temp_dir: + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: temp_dir}): + config_file_path = os.path.join(temp_dir, 'config.json') + with open(config_file_path, 'w') as f: + json.dump(config, f) + AICoreV2Client.from_env() + AICoreV2Client.__init__.assert_called_once_with(base_url=f'cfg_{self.base_url}', + auth_url=f'cfg_{self.auth_url}', + client_id=f'cfg_{self.client_id}', + cert_file_path=f'cfg_{self.cert_file_path}', + key_file_path=f'cfg_{self.key_file_path}') + + AICoreV2Client.__init__ = aicv2c_init + + @patch.dict(os.environ, {VCAP_SERVICES_ENV_VAR: VCAP_SERVICE_X509_ENV_VALUE}) + def test_x509_from_vcap(self): + vcap_dict_credentials = VCAP_SERVICE_X509_DICT[VCAP_AICORE_SERVICE_NAME][0]['credentials'] + init_mock = MagicMock(return_value=None) + aicv2c_init = AICoreV2Client.__init__ + AICoreV2Client.__init__ = init_mock + + AICoreV2Client.from_env() + + AICoreV2Client.__init__.assert_called_once_with( + base_url=f'{vcap_dict_credentials["serviceurls"]["AI_API_URL"]}/v2', + auth_url=f'{vcap_dict_credentials["certurl"]}/oauth/token', + client_id=vcap_dict_credentials['clientid'], + cert_str=vcap_dict_credentials['certificate'], + key_str=vcap_dict_credentials['key']) + + AICoreV2Client.__init__ = aicv2c_init + + @patch.dict(os.environ, {"AICORE_HOME": "XXX"}, clear=False) + def test_from_env(self): + """Test from_env with single-source credential resolution. + + The credential resolution priority is: + - Once a source (kwargs, env, config, VCAP) has any credential, ALL credentials come from that source + - Resource group is resolved separately with first source wins logic + """ + init_mock = MagicMock(return_value=None) + aicv2c_init = AICoreV2Client.__init__ + + try: + AICoreV2Client.__init__ = init_mock + + # Test 1: All parameters provided via kwargs - should use kwargs as source + AICoreV2Client.from_env(**self.full_params) + AICoreV2Client.__init__.assert_called_once_with(**self.full_params) + init_mock.reset_mock() + + # Test 2: Credentials from env variables (no kwargs credentials) + env_credentials = { + 'AICORE_BASE_URL': 'https://env-api.com/v2', + 'AICORE_AUTH_URL': 'https://env-auth.com/oauth/token', + 'AICORE_CLIENT_ID': 'env_client_id', + 'AICORE_CLIENT_SECRET': 'env_client_secret', + } + with patch.dict(os.environ, env_credentials, clear=False): + AICoreV2Client.from_env() + AICoreV2Client.__init__.assert_called_once_with( + base_url='https://env-api.com/v2', + auth_url='https://env-auth.com/oauth/token', + client_id='env_client_id', + client_secret='env_client_secret', + ) + init_mock.reset_mock() + + # Test 3: Credentials from config file + with tempfile.TemporaryDirectory() as temp_dir: + config_credentials = { + 'AICORE_BASE_URL': 'https://config-api.com/v2', + 'AICORE_AUTH_URL': 'https://config-auth.com/oauth/token', + 'AICORE_CLIENT_ID': 'config_client_id', + 'AICORE_CLIENT_SECRET': 'config_client_secret', + } + config_file_path = os.path.join(temp_dir, 'config.json') + with open(config_file_path, 'w') as f: + json.dump(config_credentials, f) + + with patch.dict(os.environ, {'AICORE_HOME': temp_dir}, clear=False): + AICoreV2Client.from_env() + AICoreV2Client.__init__.assert_called_once_with( + base_url='https://config-api.com/v2', + auth_url='https://config-auth.com/oauth/token', + client_id='config_client_id', + client_secret='config_client_secret', + ) + init_mock.reset_mock() + + # Test 4: Named profile config file + with tempfile.TemporaryDirectory() as temp_dir: + profile_name = 'test' + profile_credentials = { + 'AICORE_BASE_URL': 'https://profile-api.com/v2', + 'AICORE_AUTH_URL': 'https://profile-auth.com/oauth/token', + 'AICORE_CLIENT_ID': 'profile_client_id', + 'AICORE_CLIENT_SECRET': 'profile_client_secret', + } + config_file_path = os.path.join(temp_dir, f'config_{profile_name}.json') + with open(config_file_path, 'w') as f: + json.dump(profile_credentials, f) + + with patch.dict(os.environ, {'AICORE_HOME': temp_dir}, clear=False): + AICoreV2Client.from_env(profile_name=profile_name) + AICoreV2Client.__init__.assert_called_once_with( + base_url='https://profile-api.com/v2', + auth_url='https://profile-auth.com/oauth/token', + client_id='profile_client_id', + client_secret='profile_client_secret', + ) + init_mock.reset_mock() + + # Test 5: Resource group override - resource_group from kwargs overrides config + with tempfile.TemporaryDirectory() as temp_dir: + config_with_rg = { + 'AICORE_BASE_URL': 'https://config-api.com/v2', + 'AICORE_AUTH_URL': 'https://config-auth.com/oauth/token', + 'AICORE_CLIENT_ID': 'config_client_id', + 'AICORE_CLIENT_SECRET': 'config_client_secret', + 'AICORE_RESOURCE_GROUP': 'config_rg', + } + config_file_path = os.path.join(temp_dir, 'config.json') + with open(config_file_path, 'w') as f: + json.dump(config_with_rg, f) + + with patch.dict(os.environ, {'AICORE_HOME': temp_dir}, clear=False): + # Pass resource_group via kwargs - should override config + AICoreV2Client.from_env(resource_group='kwargs_rg') + AICoreV2Client.__init__.assert_called_once_with( + base_url='https://config-api.com/v2', + auth_url='https://config-auth.com/oauth/token', + client_id='config_client_id', + client_secret='config_client_secret', + resource_group='kwargs_rg', + ) + init_mock.reset_mock() + finally: + AICoreV2Client.__init__ = aicv2c_init + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/core/tests/ai_core_client/test_cli.py b/packages/core/tests/ai_core_client/test_cli.py new file mode 100644 index 0000000..5444d98 --- /dev/null +++ b/packages/core/tests/ai_core_client/test_cli.py @@ -0,0 +1,69 @@ +import json +import pathlib +import os +import unittest +from unittest import TestCase +from unittest.mock import patch +import tempfile + +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.helpers import get_home +from ai_core_sdk.helpers.constants import HOME_PATH_ENV_VAR + +from click.testing import CliRunner + + +AICORE_DUMMY_KEY = { + 'serviceurls': { + 'AI_API_URL': 'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com' + }, + 'appname': '', + 'clientid': '!!!', + 'clientsecret': '???', + 'identityzone': 'xxx', + 'identityzoneid': '', + 'url': 'https://xxx.authentication.sap.hana.ondemand.com' +} + +class TestAICoreCLI(TestCase): + pass + + def test_from_env(self): + with tempfile.TemporaryDirectory() as temp_dir: + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: temp_dir}): + from ai_core_sdk.cli import cli + runner = CliRunner() + temp_dir = pathlib.Path(temp_dir) + aicore_key_file = temp_dir / 'aicore-key.json' + with open(aicore_key_file, 'w') as stream: + json.dump(AICORE_DUMMY_KEY, stream) + result = runner.invoke(cli, [f'configure', '-k', aicore_key_file, '-g', 'default']) + print(result) + if result.exception: + raise result.exception + assert result.exit_code == 0, (result.output, result.exit_code) + with (pathlib.Path(get_home()) / 'config.json').open() as stream: + creds = json.load(stream) + assert creds['AICORE_AUTH_URL'] == f'{AICORE_DUMMY_KEY["url"]}/oauth/token' + assert creds['AICORE_BASE_URL'] == f'{AICORE_DUMMY_KEY["serviceurls"]["AI_API_URL"]}/v2' + assert creds['AICORE_CLIENT_ID'] == AICORE_DUMMY_KEY['clientid'] + assert creds['AICORE_CLIENT_SECRET'] == AICORE_DUMMY_KEY['clientsecret'] + AICoreV2Client.from_env() + + def test_from_input(self): + with tempfile.TemporaryDirectory() as temp_dir: + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: temp_dir}): + from ai_core_sdk.cli import cli + runner = CliRunner() + result = runner.invoke(cli, [f'configure', '-s', AICORE_DUMMY_KEY['clientsecret'], + '-i', AICORE_DUMMY_KEY['clientid'], + '-u', AICORE_DUMMY_KEY['url']], + input='https://***.ml.hana.ondemand.com' + ) + print(result) + + print('done') + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/core/tests/ai_core_client/test_credentials.py b/packages/core/tests/ai_core_client/test_credentials.py new file mode 100644 index 0000000..26b779d --- /dev/null +++ b/packages/core/tests/ai_core_client/test_credentials.py @@ -0,0 +1,309 @@ +from typing import Final, List +import json +import os +import pathlib +import shutil +import tempfile +import unittest +from unittest.mock import patch, MagicMock, call + +from ai_core_sdk.credentials import ( + CredentialsValue, + Service, + VCAPEnvironment, + fetch_credentials, + init_conf, CORE_CREDENTIAL_VALUES, +) +from ai_core_sdk.helpers.constants import (AI_CORE_PREFIX, HOME_PATH_ENV_VAR, PROFILE_ENV_VAR, VCAP_SERVICES_ENV_VAR, + VCAP_AICORE_SERVICE_NAME, CONFIG_FILE_ENV_VAR) + +VCAP_SERVICE_DICT = { + VCAP_AICORE_SERVICE_NAME: [{ + 'label': VCAP_AICORE_SERVICE_NAME, + 'name': f'{VCAP_AICORE_SERVICE_NAME}-instance', + 'instance_guid': '53ad5b47-a49a-4fec-9f0b-cd921c00b828', + 'credentials': { + 'serviceurls': { + 'AI_API_URL': 'vcap-api-url' + }, + 'url': 'vcap-auth-url', + 'clientid': 'vcap-clientid', + 'clientsecret': 'vcap-clientsecret' + } + }] +} +VCAP_SERVICE_ENV_VALUE = json.dumps(VCAP_SERVICE_DICT, indent=4) + +VCAP_SERVICE_X509_DICT = { + VCAP_AICORE_SERVICE_NAME: [{ + 'label': VCAP_AICORE_SERVICE_NAME, + 'name': f'{VCAP_AICORE_SERVICE_NAME}-instance', + 'instance_guid': '53ad5b47-a49a-4fec-9f0b-cd921c00b828', + 'credentials': { + 'serviceurls': { + 'AI_API_URL': 'vcap-api-url' + }, + 'certurl': 'vcap-cert-url', + 'clientid': 'vcap-clientid', + 'key': 'vcap-key', + 'certificate': 'vcap-certificate' + } + }] +} +VCAP_SERVICE_X509_ENV_VALUE = json.dumps(VCAP_SERVICE_X509_DICT, indent=4) + + +class TestVCAPServices(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.vcap_dict = VCAP_SERVICE_DICT[VCAP_AICORE_SERVICE_NAME][0] + + def test_vcap_services(self): + with patch.dict(os.environ, {VCAP_SERVICES_ENV_VAR: VCAP_SERVICE_ENV_VALUE}): + vcap_services = VCAPEnvironment.from_env() + self.assertTrue(all(isinstance(srv, Service) for srv in vcap_services.services)) + self.assertEqual(len(vcap_services.services), 1) + aicore_vcap_service = vcap_services[VCAP_AICORE_SERVICE_NAME] + self.assertIsInstance(aicore_vcap_service, Service) + self.assertEqual(aicore_vcap_service, vcap_services.get_service(VCAP_AICORE_SERVICE_NAME)) + self.assertEqual(len(vcap_services.get_service(VCAP_AICORE_SERVICE_NAME, exactly_one=False)), 1) + self.assertEqual(aicore_vcap_service, vcap_services.get_service_by_name(self.vcap_dict['name'])) + self.assertEqual(aicore_vcap_service.label, VCAP_AICORE_SERVICE_NAME) + self.assertEqual(aicore_vcap_service['instance_guid'], self.vcap_dict['instance_guid']) + self.assertEqual(aicore_vcap_service['credentials.clientid'], self.vcap_dict['credentials']['clientid']) + self.assertEqual(aicore_vcap_service['credentials', 'clientid'], self.vcap_dict['credentials']['clientid']) + self.assertEqual(aicore_vcap_service['credentials.clientsecret'], self.vcap_dict['credentials']['clientsecret']) + self.assertEqual(aicore_vcap_service['credentials.url'], self.vcap_dict['credentials']['url']) + with self.assertRaises(KeyError): + _ = aicore_vcap_service['non-existing'] + self.assertIsNone(aicore_vcap_service.get('non-existing', None)) + + +def assert_logging_calls(mock_logger, resolved_values, default_keys, source): + expected_calls = [] + for cred in CORE_CREDENTIAL_VALUES: + if cred.name in resolved_values: + expected_calls.append( + call('Using source %s for %s', source, cred.name)) + elif cred.name in default_keys: + expected_calls.append(call('Using source %s for %s', 'default value', cred.name)) + + # Assert the calls + mock_logger.debug.assert_has_calls(expected_calls) + + +class TestConfigHandling(unittest.TestCase): + """TestCase for init_conf and from_conf functions.""" + + @classmethod + def setUpClass(cls): + # Create a temporary directory + cls.temp_dir = pathlib.Path(tempfile.mkdtemp()) + + # Define the file name and content + cls.file_name = 'config.json' + cls.profile = 'test' + cls.file_name_profile = f'config_{cls.profile}.json' + cls.default_config = { + f'{AI_CORE_PREFIX}_CLIENT_ID': 'default-client-id', + f'{AI_CORE_PREFIX}_CLIENT_SECRET': 'default-client-secret', + f'{AI_CORE_PREFIX}_BASE_URL': 'default-base-url', + f'{AI_CORE_PREFIX}_AUTH_URL': 'default-auth-url' + } + cls.profile_config = { + f'{AI_CORE_PREFIX}_CLIENT_ID': 'profile-client-id', + f'{AI_CORE_PREFIX}_CLIENT_SECRET': 'profile-client-secret', + f'{AI_CORE_PREFIX}_BASE_URL': 'profile-base-url' + } + + # Create a file within this directory + with (cls.temp_dir / cls.file_name).open('w') as file: + json.dump(cls.default_config, file) + with (cls.temp_dir / cls.file_name_profile).open('w') as file: + json.dump(cls.profile_config, file) + + @classmethod + def tearDownClass(cls): + # Clean up the directory after all tests + shutil.rmtree(str(cls.temp_dir)) + + @patch('ai_core_sdk.credentials.logger') + def test_init_conf(self, mock_logger): + mock_logger.debug = MagicMock() + + # if no default config found return empty conf + conf = init_conf() + self.assertDictEqual(conf, {}) + + # if an explicit profile is request but the config does not exist raise an error + with self.assertRaises(FileNotFoundError): + conf = init_conf('MOCK_LLM') + + # load default config + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: str(self.temp_dir)}): + conf = init_conf() + self.assertDictEqual(conf, self.default_config) + mock_logger.debug.assert_called_with('Config file path %s', self.temp_dir / 'config.json') + + # load profile config + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: str(self.temp_dir), PROFILE_ENV_VAR: self.profile}): + conf = init_conf() + self.assertDictEqual(conf, self.profile_config) + mock_logger.debug.assert_called_with('Config file path %s', self.temp_dir / self.file_name_profile) + + # load profile config with profile param + with patch.dict(os.environ, {HOME_PATH_ENV_VAR: str(self.temp_dir)}): + conf = init_conf(profile=self.profile) + self.assertDictEqual(conf, self.profile_config) + mock_logger.debug.assert_called_with('Config file path %s', self.temp_dir / self.file_name_profile) + + # load profile config via env variable + with patch.dict(os.environ, {f'{AI_CORE_PREFIX}_PROFILE': self.profile, + HOME_PATH_ENV_VAR: str(self.temp_dir)}): + conf = init_conf() + self.assertDictEqual(conf, self.profile_config) + # overwrite env variable with explicit profile + conf = init_conf(profile='default') + self.assertDictEqual(conf, self.default_config) + mock_logger.debug.assert_called_with('Config file path %s', self.temp_dir / 'config.json') + + @patch.dict(os.environ, {VCAP_SERVICES_ENV_VAR: VCAP_SERVICE_ENV_VALUE}) + @patch('ai_core_sdk.credentials.logger') + def test_fetch_credentials_from_vcap_services(self, mock_logger): + mock_logger.debug = MagicMock() + + vcap_dict_credentials = VCAP_SERVICE_DICT[VCAP_AICORE_SERVICE_NAME][0]['credentials'] + credentials = fetch_credentials() + self.assertEqual(credentials['base_url'], f'{vcap_dict_credentials["serviceurls"]["AI_API_URL"]}/v2') + self.assertEqual(credentials['client_secret'], vcap_dict_credentials['clientsecret']) + self.assertEqual(credentials['client_id'], vcap_dict_credentials['clientid']) + self.assertEqual(credentials['auth_url'], f'{vcap_dict_credentials["url"]}/oauth/token') + + mock_logger.debug.assert_any_call("Using credentials from: VCAP service") + mock_logger.debug.assert_any_call("No resource_group found in any source") + + @patch.dict(os.environ, {VCAP_SERVICES_ENV_VAR: VCAP_SERVICE_X509_ENV_VALUE}) + @patch('ai_core_sdk.credentials.logger') + def test_fetch_credentials_from_vcap_services_with_x509_env_var(self, mock_logger): + mock_logger.debug = MagicMock() + vcap_dict_credentials = VCAP_SERVICE_X509_DICT[VCAP_AICORE_SERVICE_NAME][0]['credentials'] + credentials = fetch_credentials() + self.assertEqual(credentials['base_url'], f'{vcap_dict_credentials["serviceurls"]["AI_API_URL"]}/v2') + self.assertEqual(credentials['client_id'], vcap_dict_credentials['clientid']) + self.assertEqual(credentials['key_str'], vcap_dict_credentials['key']) + self.assertEqual(credentials['cert_str'], vcap_dict_credentials['certificate']) + self.assertEqual(credentials['auth_url'], f'{vcap_dict_credentials["certurl"]}/oauth/token') + + mock_logger.debug.assert_any_call("Using credentials from: VCAP service") + mock_logger.debug.assert_any_call("No resource_group found in any source") + + @patch('ai_core_sdk.credentials.logger') + def test_fetch_credentials_from_env(self, mock_logger): + mock_logger.debug = MagicMock() + + with patch.dict( + os.environ, + { + f'{AI_CORE_PREFIX}_CLIENT_ID': self.default_config[f'{AI_CORE_PREFIX}_CLIENT_ID'], + f'{AI_CORE_PREFIX}_CLIENT_SECRET': self.default_config[f'{AI_CORE_PREFIX}_CLIENT_SECRET'], + f'{AI_CORE_PREFIX}_BASE_URL': self.default_config[f'{AI_CORE_PREFIX}_BASE_URL'], + f'{AI_CORE_PREFIX}_AUTH_URL': self.default_config[f'{AI_CORE_PREFIX}_AUTH_URL'], + } + ): + credentials = fetch_credentials() + self.assertEqual(credentials['client_id'], self.default_config[f'{AI_CORE_PREFIX}_CLIENT_ID']) + self.assertEqual(credentials['client_secret'], self.default_config[f'{AI_CORE_PREFIX}_CLIENT_SECRET']) + self.assertEqual(credentials['base_url'], self.default_config[f'{AI_CORE_PREFIX}_BASE_URL'] + '/v2') + self.assertEqual(credentials['auth_url'], + self.default_config[f'{AI_CORE_PREFIX}_AUTH_URL'] + '/oauth/token') + + mock_logger.debug.assert_any_call("Using credentials from: environment variables") + mock_logger.debug.assert_any_call("No resource_group found in any source") + + @patch('ai_core_sdk.credentials.logger') + def test_fetch_credentials_from_config_file(self, mock_logger): + mock_logger.debug = MagicMock() + with patch.dict(os.environ, {CONFIG_FILE_ENV_VAR: str(self.temp_dir / 'config.json')}): + fetch_credentials() + + mock_logger.debug.assert_any_call("Using credentials from: config file") + mock_logger.debug.assert_any_call("No resource_group found in any source") + + @patch('ai_core_sdk.credentials.logger') + def test_fetch_credentials_from_kwargs(self, mock_logger): + mock_logger.debug = MagicMock() + + with patch.dict( + os.environ, + { + f'{AI_CORE_PREFIX}_CLIENT_ID': 'env-var-client-id', + f'{AI_CORE_PREFIX}_CLIENT_SECRET': 'env-var-client-secret', + f'{AI_CORE_PREFIX}_BASE_URL': 'env-var-base-url', + f'{AI_CORE_PREFIX}_RESOURCE_GROUP': 'env-var-resource-group', + } + ): + credentials = fetch_credentials( + client_id='kwarg-client', + client_secret='kwarg-secret', + base_url='kwarg-url', + auth_url='kwarg-auth-url' + ) + + self.assertEqual(credentials['client_id'], 'kwarg-client') + self.assertEqual(credentials['client_secret'], 'kwarg-secret') + self.assertEqual(credentials['base_url'], 'kwarg-url/v2') + self.assertEqual(credentials['auth_url'], 'kwarg-auth-url/oauth/token') + self.assertEqual(credentials['resource_group'], 'env-var-resource-group') + + mock_logger.debug.assert_any_call("Using credentials from: kwargs") + mock_logger.debug.assert_any_call( + "Using resource_group '%s' from: %s", + 'env-var-resource-group', + 'environment variables' + ) + + @patch('ai_core_sdk.credentials.logger') + def test_init_conf_permission_error(self, mock_logger): + mock_logger.warning = MagicMock() + + # Create a config file and make it unreadable + config_file = self.temp_dir / 'config_no_permission.json' + with config_file.open('w') as f: + json.dump(self.default_config, f) + + # Remove read permissions + config_file.chmod(0o000) + + try: + with patch.dict(os.environ, {CONFIG_FILE_ENV_VAR: str(config_file)}): + conf = init_conf() + # Should return empty config when permission is denied + self.assertDictEqual(conf, {}) + # Should log a warning + mock_logger.warning.assert_called_once() + warning_call_args = mock_logger.warning.call_args[0] + self.assertIn("Permission denied", warning_call_args[0]) + self.assertIn("File ignored", warning_call_args[0]) + self.assertEqual(warning_call_args[1], config_file) + finally: + # Restore permissions for cleanup in teardown + config_file.chmod(0o644) + + @patch('ai_core_sdk.credentials.logger') + def test_injecting_credential_values(self, mock_logger): + mock_logger.debug = MagicMock() + + test_credential_values = [ + CredentialsValue(name='a'), + CredentialsValue(name='b'), + CredentialsValue(name='c') + ] + + credentials = fetch_credentials(credential_values=test_credential_values, a='1', b='2', c='3', validate=False) + + self.assertEqual('1', credentials['a']) + self.assertEqual('2', credentials['b']) + self.assertEqual('3', credentials['c']) + + mock_logger.debug.assert_any_call("Using credentials from: kwargs") diff --git a/packages/core/tests/helpers/test_base_models.py b/packages/core/tests/helpers/test_base_models.py new file mode 100644 index 0000000..461a018 --- /dev/null +++ b/packages/core/tests/helpers/test_base_models.py @@ -0,0 +1,20 @@ +from unittest import TestCase + +from ai_core_sdk.models.base_models import BasicNameResponse, Message + + +class TestBaseModels(TestCase): + def test_basic_name_response_string_representation(self): + name = "dummy_name" + message = "dummy_message" + basic_name_response_object = BasicNameResponse(name=name, message=message) + self.assertIn("Name: ", basic_name_response_object.__str__()) + self.assertIn(name, basic_name_response_object.__str__()) + self.assertIn("Message: ", basic_name_response_object.__str__()) + self.assertIn(message, basic_name_response_object.__str__()) + + def test_message_string_representation(self): + message = "dummy_message" + message_object = Message(message=message) + self.assertIn("Message: ", message_object.__str__()) + self.assertIn(message, message_object.__str__()) \ No newline at end of file diff --git a/packages/core/tests/resource_clients/__init__.py b/packages/core/tests/resource_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/tests/resource_clients/resource_client_test_base.py b/packages/core/tests/resource_clients/resource_client_test_base.py new file mode 100644 index 0000000..f065536 --- /dev/null +++ b/packages/core/tests/resource_clients/resource_client_test_base.py @@ -0,0 +1,32 @@ +from typing import Any, Callable, Dict, List, Union +from unittest import TestCase +from unittest.mock import MagicMock + + +class ResourceClientTestBase(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.resource_group = 'test_resource_group' + + def setUp(self): + super().setUp() + self.rest_client_mock = MagicMock() + self.client = None + + def assert_object(self, d: Dict[str, Any], o: object): + for k in d.keys(): + self.assertEqual(d[k], getattr(o, k)) + + def assert_object_lists(self, dict_list: List[Dict[str, Any]], object_list: list, + assert_object_function: Callable = None, sort_key: str = 'id'): + if not assert_object_function: + assert_object_function = self.assert_object + self.assertEqual(len(dict_list), len(object_list)) + dict_list = sorted(dict_list, key=lambda ad: ad[sort_key]) + object_list = sorted(object_list, key=lambda a: getattr(a, sort_key)) + for i in range(len(object_list)): + assert_object_function(dict_list[i], object_list[i]) + + def assert_all_attributes_none(self, o: object): + for k, v in o.__dict__.items(): + self.assertIsNone(v) diff --git a/packages/core/tests/resource_clients/test_applications_client.py b/packages/core/tests/resource_clients/test_applications_client.py new file mode 100644 index 0000000..15a00d8 --- /dev/null +++ b/packages/core/tests/resource_clients/test_applications_client.py @@ -0,0 +1,185 @@ +import copy + +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.exception import AICoreInvalidInputException +from ai_core_sdk.models.application import Application +from ai_core_sdk.models.application_source import ApplicationSource +from ai_core_sdk.models.application_status import ApplicationStatus +from ai_core_sdk.resource_clients.applications_client import ApplicationsClient + + +class TestApplicationsClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ApplicationsClient(self.rest_client_mock) + self.app_path = '/admin/applications' + + @staticmethod + def create_application_dict(): + return { + 'revision': 'test_revision', + 'path': 'test_path', + 'application_name': 'test_app_name', + 'repository_url': 'test_repo_url' + } + + @staticmethod + def get_application_status_dict(): + return { + "health_status": "test_health_status", + "sync_status": "test_sync_status", + "message": "test_status_message", + "source": { + "repo_url": "test_source_repo_url", + "path": "test_source_path", + "revision": "test_source_revision" + }, + "sync_finished_at": "test_sync_finished_at", + "sync_started_at": "test_sync_started_at", + "reconciled_at": "test_reconciled_at", + "sync_resources_status": [ + { + "name": "test_resource_name", + "kind": "test_resource_kind", + "status": "test_resource_sync_status", + "message": "test_resource_sync_message" + } + ] + } + + def assert_application(self, app_dict: dict, app: Application): + app_dict_c = copy.deepcopy(app_dict) + self.assertTrue(app_dict_c['application_name'] in app.application_name) + del app_dict_c['application_name'] + self.assert_object(app_dict_c, app) + + @staticmethod + def prepend_tenant_hash(s: str): + return f'tenant_hash-{s}' + + def assert_application_source(self, source_dict: dict, source: ApplicationSource): + self.assertEqual(source_dict['repo_url'], source.repourl) + del source_dict['repo_url'] + self.assert_object(source_dict, source) + self.assertIn("ApplicationSource repourl: ", source.__str__()) + self.assertIn(source.repourl, source.__str__()) + self.assertIn("ApplicationSource revision: ", source.__str__()) + self.assertIn(source.revision, source.__str__()) + + def assert_application_status(self, status_dict: dict, status: ApplicationStatus): + sd = copy.deepcopy(status_dict) + self.assert_application_source(sd['source'], status.source) + del sd['source'] + self.assert_object_lists(sd['sync_resources_status'], status.sync_resources_status, sort_key='name') + self.assert_object_lists(sd['sync_resources_status'], status.sync_ressources_status, sort_key='name') + del sd['sync_resources_status'] + self.assert_object(sd, status) + self.assertIn("ApplicationStatus health status: ", status.__str__()) + self.assertIn(sd['health_status'], status.__str__()) + self.assertIn("ApplicationStatus sync status: ", status.__str__()) + self.assertIn(sd['sync_status'], status.__str__()) + self.assertIn("ApplicationStatus message:", status.__str__()) + self.assertIn(sd['message'], status.__str__()) + self.assertIn("ApplicationSource repourl: ", status.source.__str__()) + self.assertIn(status.source.repourl, status.source.__str__()) + self.assertIn("ApplicationSource revision: ", status.source.__str__()) + self.assertIn(status.source.revision, status.source.__str__()) + + def _test_create_application(self, app_dict: dict): + response_message = 'application has been been created' + self.rest_client_mock.post.return_value = {'id': self.prepend_tenant_hash(app_dict['application_name']), + 'message': response_message} + response = self.client.create(**app_dict) + self.rest_client_mock.post.assert_called_with(path=self.app_path, body=app_dict) + self.assertEqual(response_message, response.message) + + def test_create_application_with_repository_url(self): + app_dict = self.create_application_dict() + self._test_create_application(app_dict) + + def test_create_application_with_repository_name(self): + app_dict = self.create_application_dict() + del app_dict['repository_url'] + app_dict['repository_name'] = 'test_repository_name' + self._test_create_application(app_dict) + + def test_create_application_fails_with_both_name_and_url(self): + app_dict = self.create_application_dict() + app_dict['repository_name'] = 'test_repository_name' + with self.assertRaises(AICoreInvalidInputException): + self.client.create(**app_dict) + self.rest_client_mock.get.assert_not_called() + + def test_create_application_fails_with_no_name_no_url(self): + app_dict = self.create_application_dict() + del app_dict['repository_url'] + with self.assertRaises(AICoreInvalidInputException): + self.client.create(**app_dict) + self.rest_client_mock.assert_not_called() + + def test_get_application(self): + app_dict = self.create_application_dict() + self.rest_client_mock.get.return_value = app_dict.copy() + app = self.client.get(application_name=app_dict['application_name']) + self.rest_client_mock.get.assert_called_with(path=f'{self.app_path}/{app_dict["application_name"]}') + self.assert_object(app_dict, app) + self.assertIn("Application name: ", app.__str__()) + self.assertIn(app_dict['application_name'], app.__str__()) + + def test_get_application_status(self): + app_name = 'test_app_name' + status_dict = self.get_application_status_dict() + self.rest_client_mock.get.return_value = status_dict.copy() + app_status = self.client.get_status(application_name=app_name) + self.rest_client_mock.get.assert_called_with(path=f'{self.app_path}/{app_name}/status') + self.assert_application_status(status_dict, app_status) + + def test_get_application_status_bare_minimum(self): + app_name = 'test_app_bare_minimum' + self.rest_client_mock.get.return_value = {} + app_status = self.client.get_status(application_name=app_name) + self.rest_client_mock.get.assert_called_with(path=f'{self.app_path}/{app_name}/status') + self.assert_all_attributes_none(app_status) + + def test_query_applications(self): + n = 3 + app_dicts = [self.create_application_dict() for _ in range(n)] + response_ads = [] + for ad in app_dicts: + adc = ad.copy() + adc['application_name'] = self.prepend_tenant_hash(ad["application_name"]) + response_ads.append(adc) + self.rest_client_mock.get.return_value = {'resources': response_ads, 'count': n} + app_qr = self.client.query() + self.rest_client_mock.get.assert_called_with(path=self.app_path) + self.assert_object_lists(app_dicts, app_qr.resources, sort_key='application_name', + assert_object_function=self.assert_application) + + def test_modify_application(self): + app_dict = self.create_application_dict() + response_dict = {'id': self.prepend_tenant_hash(app_dict['application_name']), + 'message': 'application has been updated'} + self.rest_client_mock.patch.return_value = response_dict + response = self.client.modify(**app_dict) + body = app_dict.copy() + del body['application_name'] + self.rest_client_mock.patch.assert_called_with(path=f'{self.app_path}/{app_dict["application_name"]}', + body=body) + self.assert_object(response_dict, response) + + def test_delete_application(self): + app_name = 'test_app_name' + response_dict = {'id': self.prepend_tenant_hash(app_name), 'message': 'application deleted'} + self.rest_client_mock.delete.return_value = response_dict + response = self.client.delete(application_name=app_name) + self.rest_client_mock.delete.assert_called_with(path=f'{self.app_path}/{app_name}') + self.assert_object(response_dict, response) + + def test_refresh_application(self): + app_name = 'test_app_name' + response_dict = {'id': self.prepend_tenant_hash(app_name), + 'message': 'A refresh of the application has been scheduled.'} + self.rest_client_mock.post.return_value = response_dict + response = self.client.refresh(application_name=app_name) + self.rest_client_mock.post.assert_called_with(path=f'{self.app_path}/{app_name}/refresh') + self.assert_object(response_dict, response) diff --git a/packages/core/tests/resource_clients/test_docker_registry_secrets_client.py b/packages/core/tests/resource_clients/test_docker_registry_secrets_client.py new file mode 100644 index 0000000..0a17a58 --- /dev/null +++ b/packages/core/tests/resource_clients/test_docker_registry_secrets_client.py @@ -0,0 +1,77 @@ +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.models.docker_registry_secret import DockerRegistrySecret +from ai_core_sdk.resource_clients.docker_registry_secrets_client import DockerRegistrySecretsClient + + +class TestDockerRegistrySecretsClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = DockerRegistrySecretsClient(self.rest_client_mock) + self.drs_path = '/admin/dockerRegistrySecrets' + + def assert_docker_registry_secret(self, drs_dict: dict, drs: DockerRegistrySecret): + self.assertEqual(drs_dict['name'], drs.name) + self.assertIn("DockerRegistrySecret name: ", drs.__str__()) + self.assertIn(drs_dict['name'], drs.__str__()) + + @staticmethod + def create_docker_registry_secret_dict(): + return { + 'name': 'test_docker_registry_secret_name', + 'data': { + ".dockerconfigjson": "{\"auths\": {\"test_docker_registry_url\": {\"username\": \"test_docker_username\", \"password\": \"test_docker_password\"}}}" + } + } + + def test_get_docker_registry_secret(self): + drs_dict = self.create_docker_registry_secret_dict() + self.rest_client_mock.get.return_value = drs_dict.copy() + drs = self.client.get(name=drs_dict['name']) + self.rest_client_mock.get.assert_called_with(path=f'{self.drs_path}/{drs_dict["name"]}') + self.assertEqual(drs_dict['name'], drs.name) + + def test_query_docker_registry_secrets(self): + n = 3 + drs_dicts = [self.create_docker_registry_secret_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [dd.copy() for dd in drs_dicts], 'count': n} + drs_qr = self.client.query() + self.rest_client_mock.get.assert_called_with(path=self.drs_path, params=None) + self.assert_object_lists(drs_dicts, drs_qr.resources, sort_key='name', + assert_object_function=self.assert_docker_registry_secret) + + params = {'top': 5, 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(**params) + params['$top'] = params['top'] + del params['top'] + params['$skip'] = params['skip'] + del params['skip'] + self.rest_client_mock.get.assert_called_with(path=self.drs_path, params=params) + + def test_create_docker_registry_secret(self): + drs_dict = self.create_docker_registry_secret_dict() + response_message = 'secret has been been created' + self.rest_client_mock.post.return_value = {'message': response_message} + response = self.client.create(name=drs_dict['name'], data=drs_dict['data']) + body = {'name': drs_dict['name'], 'data': drs_dict['data']} + self.rest_client_mock.post.assert_called_with(path=self.drs_path, body=body) + self.assertEqual(response_message, response.message) + + def test_modify_deployment(self): + drs_dict = self.create_docker_registry_secret_dict() + response_dict = {'id': drs_dict['name'], 'message': 'Secret has been modified'} + body = {'data': drs_dict['data']} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(name=drs_dict['name'], **body) + self.rest_client_mock.patch.assert_called_with(path=f'{self.drs_path}/{drs_dict["name"]}', body=body) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_delete_deployment(self): + test_drs_name = 'test_docker_registry_secret_name' + response_dict = {'id': test_drs_name, 'message': 'Deployment deleted'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(name=test_drs_name) + self.rest_client_mock.delete.assert_called_with(path=f'{self.drs_path}/{test_drs_name}') + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) diff --git a/packages/core/tests/resource_clients/test_kpis_client.py b/packages/core/tests/resource_clients/test_kpis_client.py new file mode 100644 index 0000000..c207646 --- /dev/null +++ b/packages/core/tests/resource_clients/test_kpis_client.py @@ -0,0 +1,37 @@ +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.resource_clients.kpi_client import KpiClient + + +class TestKpiClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = KpiClient(self.rest_client_mock) + self.url_prefix = "/analytics/kpis" + + @staticmethod + def get_kpi_response(): + return { + "header": [ + "ResourceGroup", + "Executions", + "Artifacts", + "Deployments" + ], + "rows": [ + [ + "00112233-4455-6677-8899-aabbccddeeff", + 30, + 30, + 3 + ] + ] + } + + def test_get_kpi(self): + kpi_data = self.get_kpi_response() + self.rest_client_mock.get.return_value = kpi_data + kpi_response = self.client.query() + self.rest_client_mock.get.assert_called_with(path=f'{self.url_prefix}') + self.assert_object(kpi_data, kpi_response) + self.assertIn("KPIs header(s): ", kpi_response.__str__()) + self.assertIn(", ".join(kpi_data['header']), kpi_response.__str__()) diff --git a/packages/core/tests/resource_clients/test_metrics_client.py b/packages/core/tests/resource_clients/test_metrics_client.py new file mode 100644 index 0000000..815dc0a --- /dev/null +++ b/packages/core/tests/resource_clients/test_metrics_client.py @@ -0,0 +1,235 @@ +from unittest.mock import MagicMock + +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_label import MetricLabel +from ai_api_client_sdk.models.metric_tag import MetricTag + +from ai_core_sdk.exception import AICoreSDKException +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.resource_clients.metrics_client import MetricsCoreClient + + +class TestMetricsCoreClient(ResourceClientTestBase): + def setUp(self): + self.rest_client_mock = MagicMock() + self.metrics_client = MetricsCoreClient(self.rest_client_mock) + + @staticmethod + def __patch_metrics_body(execution_id): + patch_mb = { + "execution_id": execution_id, + "metrics": [ + { + "name": "Test Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Test Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Test Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + return patch_mb + + @staticmethod + def __patch_metrics_body_with_artifact_label(execution_id): + patch_mb = { + "execution_id": execution_id, + "metrics": [ + { + "name": "Test Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + }, { + "name": "metrics.ai.sap.com/Artifact.name", + "value": "test_artifact" + } + ] + } + ], + "tags": [ + { + "name": "Test Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Test Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + return patch_mb + + def test_modify_metrics(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = {'execution_id': metrics_patch_data.get('execution_id'), + 'metrics': [Metric.from_dict(md) for md in metrics_patch_data['metrics']], + 'tags': [MetricTag.from_dict(mtd) for mtd in metrics_patch_data['tags']], + 'custom_info': [MetricCustomInfo.from_dict(mcid) for mcid in metrics_patch_data['custom_info']]} + self.metrics_client.modify(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body=self.__patch_metrics_body(execution_id='test_execution_id'), + resource_group=self.resource_group) + + def test_log_metrics(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'metrics': [Metric.from_dict(md) for md in metrics_patch_data['metrics']], + 'artifact_name': 'test_artifact' + } + self.metrics_client.log_metrics(**body, resource_group=self.resource_group) + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'metrics':self.__patch_metrics_body_with_artifact_label(execution_id='test_execution_id')['metrics'] + }, + resource_group=self.resource_group) + + def test_set_custom_info(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'custom_info': [MetricCustomInfo.from_dict(md) for md in metrics_patch_data['custom_info']], + } + self.metrics_client.set_custom_info(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'custom_info':self.__patch_metrics_body(execution_id='test_execution_id')['custom_info'] + }, + resource_group=self.resource_group) + + def test_set_tags(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'tags': [MetricTag.from_dict(md) for md in metrics_patch_data['tags']], + } + self.metrics_client.set_tags(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'tags':self.__patch_metrics_body(execution_id='test_execution_id')['tags'] + }, + resource_group=self.resource_group) + + def test_modify_uses_passed_execution_id_over_instance(self): + client = MetricsCoreClient(self.rest_client_mock, execution_id="default_exec_id") + + metrics_patch_data = self.__patch_metrics_body(execution_id='method_exec_id') + metrics = [Metric.from_dict(md) for md in metrics_patch_data['metrics']] + + client.modify(execution_id='method_exec_id', metrics=metrics, resource_group=self.resource_group) + + expected_body = { + 'execution_id': 'method_exec_id', + 'metrics': [m.to_dict() for m in metrics], + } + + self.rest_client_mock.patch.assert_called_with( + path='/metrics', + body=expected_body, + resource_group=self.resource_group, + ) + + def test_log_metrics_uses_passed_execution_id_over_instance(self): + client = MetricsCoreClient(self.rest_client_mock, execution_id="default_exec_id") + + metrics_patch_data = self.__patch_metrics_body(execution_id='method_exec_id') + metrics = [Metric.from_dict(md) for md in metrics_patch_data['metrics']] + + client.log_metrics( + execution_id='method_exec_id', + metrics=metrics, + artifact_name='test_artifact', + resource_group=self.resource_group, + ) + + expected_metrics = [m.to_dict() for m in metrics] + + expected_body = { + 'execution_id': 'method_exec_id', + 'metrics': expected_metrics, + } + + self.rest_client_mock.patch.assert_called_with( + path='/metrics', + body=expected_body, + resource_group=self.resource_group, + ) + + def test_set_custom_info_uses_passed_execution_id_over_instance(self): + client = MetricsCoreClient(self.rest_client_mock, execution_id="default_exec_id") + + metrics_patch_data = self.__patch_metrics_body(execution_id='method_exec_id') + custom_info = [MetricCustomInfo.from_dict(ci) for ci in metrics_patch_data['custom_info']] + + client.set_custom_info( + execution_id='method_exec_id', + custom_info=custom_info, + resource_group=self.resource_group, + ) + + expected_body = { + 'execution_id': 'method_exec_id', + 'custom_info': [ci.__dict__ for ci in custom_info], + } + + self.rest_client_mock.patch.assert_called_with( + path='/metrics', + body=expected_body, + resource_group=self.resource_group, + ) + + def test_set_tags_uses_passed_execution_id_over_instance(self): + client = MetricsCoreClient(self.rest_client_mock, execution_id="default_exec_id") + + metrics_patch_data = self.__patch_metrics_body(execution_id='method_exec_id') + tags = [MetricTag.from_dict(t) for t in metrics_patch_data['tags']] + + client.set_tags( + execution_id='method_exec_id', + tags=tags, + resource_group=self.resource_group, + ) + + expected_body = { + 'execution_id': 'method_exec_id', + 'tags': [t.__dict__ for t in tags], + } + + self.rest_client_mock.patch.assert_called_with( + path='/metrics', + body=expected_body, + resource_group=self.resource_group, + ) + + diff --git a/packages/core/tests/resource_clients/test_object_store_secrets_client.py b/packages/core/tests/resource_clients/test_object_store_secrets_client.py new file mode 100644 index 0000000..1b0a091 --- /dev/null +++ b/packages/core/tests/resource_clients/test_object_store_secrets_client.py @@ -0,0 +1,125 @@ +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.models.object_store_secret import ObjectStoreSecret +from ai_core_sdk.resource_clients.object_store_secrets_client import ObjectStoreSecretsClient + + +class TestObjectStoreSecretsClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = ObjectStoreSecretsClient(self.rest_client_mock) + self.url_prefix = "/admin/objectStoreSecrets" + + def assert_object_store_secret(self, oss_dict: dict, response_oss: ObjectStoreSecret): + STORAGE_PREFIX = 'storage.ai.sap.com/' + SERVING_KUBEFLOW_PREFIX = 'serving.kubeflow.org/' + self.assertEqual(oss_dict['name'], response_oss.name) + self.assertEqual(oss_dict['name'], response_oss.name) + self.assertEqual(oss_dict['type'], response_oss.metadata[f'{STORAGE_PREFIX}type']) + self.assertEqual(oss_dict['bucket'], response_oss.metadata[f'{STORAGE_PREFIX}bucket']) + self.assertEqual(oss_dict['endpoint'], response_oss.metadata[f'{STORAGE_PREFIX}endpoint']) + self.assertEqual(oss_dict['region'], response_oss.metadata[f'{STORAGE_PREFIX}region']) + self.assertEqual(oss_dict['pathPrefix'], response_oss.metadata[f'{STORAGE_PREFIX}pathPrefix']) + self.assertEqual(oss_dict['endpoint'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-endpoint']) + self.assertEqual(oss_dict['region'], response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-region']) + self.assertEqual('1', response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-usehttps']) + self.assertEqual('0', response_oss.metadata[f'{SERVING_KUBEFLOW_PREFIX}s3-verifyssl']) + + @staticmethod + def create_object_store_secret_dict(): + return { + 'name': 'test_object_store_secret_name', + 'type': 's3', + 'bucket': 'test_bucket', + 'endpoint': 'www.example.com', + 'region': 'eu', + 'pathPrefix': 'my-api', + 'verifyssl': '0', + 'usehttps': '1', + 'data': { + "AWS_ACCESS_KEY_ID": "test", + "AWS_SECRET_ACCESS_KEY": "test" + } + } + + @staticmethod + def get_object_store_secret_response(): + return { + 'name': 'test_object_store_secret_name', + 'metadata': { + 'serving.kubeflow.org/s3-usehttps': '1', + 'serving.kubeflow.org/s3-verifyssl': '0', + 'serving.kubeflow.org/s3-endpoint': 'www.example.com', + 'serving.kubeflow.org/s3-region': 'eu', + 'storage.ai.sap.com/type': 's3', + 'storage.ai.sap.com/bucket': 'test_bucket', + 'storage.ai.sap.com/endpoint': 'www.example.com', + 'storage.ai.sap.com/region': 'eu', + 'storage.ai.sap.com/pathPrefix': 'my-api' + }, + } + + def test_get_object_store_secret(self): + oss_dict = self.create_object_store_secret_dict() + self.rest_client_mock.get.return_value = self.get_object_store_secret_response() + oss = self.client.get(name=oss_dict['name'], resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path=f'{self.url_prefix}/{oss_dict["name"]}', + resource_group=self.resource_group) + self.assert_object_store_secret(oss_dict, oss) + self.assertIn("Object store secret name: ", oss.__str__()) + self.assertIn(oss_dict['name'], oss.__str__()) + + def test_query_object_store_secrets(self): + n = 3 + oss_dicts = [self.create_object_store_secret_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [self.get_object_store_secret_response() for osd in oss_dicts], 'count': n} + oss_qr = self.client.query() + self.rest_client_mock.get.assert_called_with(path=self.url_prefix, params=None, resource_group=None) + self.assert_object_lists(oss_dicts, oss_qr.resources, sort_key='name', + assert_object_function=self.assert_object_store_secret) + + params = {'top': 5, 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(**params, resource_group=self.resource_group) + params['$top'] = params['top'] + del params['top'] + params['$skip'] = params['skip'] + del params['skip'] + self.rest_client_mock.get.assert_called_with(path=self.url_prefix, params=params, + resource_group=self.resource_group) + + def test_create_object_store_secret(self): + oss_dict = self.create_object_store_secret_dict() + response_message = 'secret has been been created' + self.rest_client_mock.post.return_value = {'name': oss_dict['name'], 'message': response_message} + oss = self.client.create(name=oss_dict['name'], type=oss_dict['type'], bucket=oss_dict['bucket'], + endpoint=oss_dict['endpoint'], region=oss_dict['region'], + path_prefix=oss_dict['pathPrefix'], verifyssl=oss_dict['verifyssl'], + usehttps=oss_dict['usehttps'], data=oss_dict['data'], + resource_group=self.resource_group) + body = {'name': oss_dict['name'], 'type': oss_dict['type'], 'bucket': oss_dict['bucket'], + 'endpoint': oss_dict['endpoint'], 'region': oss_dict['region'], 'path_prefix': oss_dict['pathPrefix'], + 'verifyssl': oss_dict['verifyssl'], 'usehttps': oss_dict['usehttps'], 'data': oss_dict['data']} + self.rest_client_mock.post.assert_called_with(path=self.url_prefix, body=body, + resource_group=self.resource_group) + self.assertEqual(response_message, oss.message) + + def test_modify_deployment(self): + oss_dict = self.create_object_store_secret_dict() + response_dict = {'id': oss_dict['name'], 'message': 'Secret has been modified'} + body = {'name': oss_dict['name'], 'type': oss_dict['type'], 'data': oss_dict['data']} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(**body, resource_group=self.resource_group) + self.rest_client_mock.patch.assert_called_with(path=f'{self.url_prefix}/{oss_dict["name"]}', body=body, + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) + + def test_delete_deployment(self): + test_oss_name = 'test_object_store_secret_name' + response_dict = {'id': test_oss_name, 'message': 'Deployment deleted'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(name=test_oss_name, resource_group=self.resource_group) + self.rest_client_mock.delete.assert_called_with(path=f'{self.url_prefix}/{test_oss_name}', + resource_group=self.resource_group) + self.assertEqual(response_dict['id'], br.id) + self.assertEqual(response_dict['message'], br.message) diff --git a/packages/core/tests/resource_clients/test_repositories_client.py b/packages/core/tests/resource_clients/test_repositories_client.py new file mode 100644 index 0000000..ebbc43c --- /dev/null +++ b/packages/core/tests/resource_clients/test_repositories_client.py @@ -0,0 +1,92 @@ +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.models.repository import Repository +from ai_core_sdk.models.repository_status import RepositoryStatus +from ai_core_sdk.resource_clients.repositories_client import RepositoriesClient + + +class TestRepositoriesClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = RepositoriesClient(self.rest_client_mock) + self.repo_path = '/admin/repositories' + + def assert_repository(self, repo_dict: dict, repo: Repository): + self.assertEqual(repo_dict['name'], repo.name) + self.assertEqual(repo_dict['url'], repo.url) + if 'status' in repo_dict: + self.assertEqual(repo_dict['status'], repo.status.value) + + @staticmethod + def create_repository_dict(): + return { + 'name': 'test_repo_name', + 'url': 'test_repo_url', + 'username': 'test_username', + 'password': 'test_password' + } + + @staticmethod + def create_repo_status_dict(): + return { + 'name': 'test_repo_name', + 'url': 'test_repo_url', + 'status': RepositoryStatus.IN_PROGRESS.value + } + + def test_create_repository(self): + repo_dict = self.create_repository_dict() + response_message = 'Repository has been on-boarded' + self.rest_client_mock.post.return_value = {'message': response_message} + response = self.client.create(name=repo_dict['name'], url=repo_dict['url'], username=repo_dict['username'], + password=repo_dict['password']) + self.rest_client_mock.post.assert_called_with(path=self.repo_path, body=repo_dict) + self.assertEqual(response_message, response.message) + + def test_delete_repository(self): + test_repo_name = 'test_repo_name' + response_dict = {'id': test_repo_name, 'message': 'Repo deleted'} + self.rest_client_mock.delete.return_value = response_dict + response = self.client.delete(name=test_repo_name) + self.rest_client_mock.delete.assert_called_with(path=f'{self.repo_path}/{test_repo_name}') + self.assertEqual(test_repo_name, response.id) + self.assertEqual(response_dict['message'], response.message) + + def test_get_repository(self): + repo_dict = self.create_repo_status_dict() + self.rest_client_mock.get.return_value = repo_dict.copy() + repo = self.client.get(name=repo_dict['name']) + self.rest_client_mock.get.assert_called_with(path=f'{self.repo_path}/{repo_dict["name"]}') + self.assert_repository(repo_dict, repo) + self.assertIn("Repository name: ", repo.__str__()) + self.assertIn(repo_dict['name'], repo.__str__()) + self.assertIn("Repository url: ", repo.__str__()) + self.assertIn(repo_dict['url'], repo.__str__()) + + def test_get_repository_bare_minimum(self): + repo_dict = self.create_repo_status_dict() + del repo_dict['status'] + self.rest_client_mock.get.return_value = repo_dict.copy() + repo = self.client.get(name=repo_dict['name']) + self.rest_client_mock.get.assert_called_with(path=f'{self.repo_path}/{repo_dict["name"]}') + self.assert_repository(repo_dict, repo) + + def test_modify_repository(self): + repo_dict = self.create_repository_dict() + response_dict = {'id': repo_dict['name'], 'message': 'Repo has been modified'} + body = {'username': repo_dict['username'], 'password': repo_dict['password']} + self.rest_client_mock.patch.return_value = response_dict + response = self.client.modify(name=repo_dict['name'], **body) + self.rest_client_mock.patch.assert_called_with(path=f'{self.repo_path}/{repo_dict["name"]}', body=body) + self.assertEqual(repo_dict['name'], response.id) + self.assertEqual(response_dict['message'], response.message) + + def test_query_repository(self): + n = 3 + repo_dicts = [self.create_repo_status_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [rd.copy() for rd in repo_dicts], 'count': n} + repo_qr = self.client.query() + self.rest_client_mock.get.assert_called_with(path=self.repo_path) + self.assert_object_lists(repo_dicts, repo_qr.resources, sort_key='name', + assert_object_function=self.assert_repository) + + diff --git a/packages/core/tests/resource_clients/test_secrets_client.py b/packages/core/tests/resource_clients/test_secrets_client.py new file mode 100644 index 0000000..1346a6a --- /dev/null +++ b/packages/core/tests/resource_clients/test_secrets_client.py @@ -0,0 +1,89 @@ +from .resource_client_test_base import ResourceClientTestBase +from ai_core_sdk.models.secret import Secret +from ai_core_sdk.resource_clients.secrets_client import SecretsClient + + +class TestSecretsClient(ResourceClientTestBase): + def setUp(self): + super().setUp() + self.client = SecretsClient(self.rest_client_mock) + self.url_prefix = "/admin/secrets" + + def assert_secret(self, secret_dict: dict, response_secret: Secret): + self.assertEqual(secret_dict['name'], response_secret.name) + self.assertEqual(secret_dict['data'], response_secret.data) + + @staticmethod + def create_secret_dict(): + return { + 'name': 'test_secret_name', + 'data': { + "AWS_ACCESS_KEY_ID": "test", + "AWS_SECRET_ACCESS_KEY": "test" + } + } + + @staticmethod + def get_secret_response(): + return { + 'name': 'test_secret_name', + 'data': { + "AWS_ACCESS_KEY_ID": "test", + "AWS_SECRET_ACCESS_KEY": "test" + }, + } + + def test_query_secrets(self): + n = 3 + secret_dicts = [self.create_secret_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': [self.get_secret_response() for secret in secret_dicts], + 'count': n} + secret_gr = self.client.query() + self.rest_client_mock.get.assert_called_with(path=self.url_prefix, params=None, + headers={'AI-Tenant-Scope': 'true'}, resource_group=None) + self.assert_object_lists(secret_dicts, secret_gr.resources, sort_key='name', + assert_object_function=self.assert_secret) + + params = {'top': 5, 'skip': 1} + self.rest_client_mock.get.return_value = {'resources': [], 'count': 0} + self.client.query(**params, resource_group=self.resource_group) + params['$top'] = params['top'] + del params['top'] + params['$skip'] = params['skip'] + del params['skip'] + self.rest_client_mock.get.assert_called_with(path=self.url_prefix, params=params, + resource_group=self.resource_group, + headers={'AI-Tenant-Scope': 'true'}) + + def test_create_secret(self): + secret_dict = self.create_secret_dict() + response_message = 'secret has been been created' + self.rest_client_mock.post.return_value = {'name': secret_dict['name'], 'message': response_message} + secrets = self.client.create(name=secret_dict['name'], data=secret_dict['data'], + resource_group=self.resource_group) + body = {'name': secret_dict['name'], 'data': secret_dict['data']} + self.rest_client_mock.post.assert_called_with(path=self.url_prefix, body=body, + resource_group=self.resource_group, + headers={'AI-Tenant-Scope': 'true'}) + self.assertEqual(response_message, secrets.message) + + def test_modify_secret(self): + secret_dict = self.create_secret_dict() + response_dict = {'message': 'Secret has been modified'} + body = {'data': secret_dict['data']} + self.rest_client_mock.patch.return_value = response_dict + br = self.client.modify(name=secret_dict['name'], **body, resource_group=self.resource_group) + self.rest_client_mock.patch.assert_called_with(path=f'{self.url_prefix}/{secret_dict["name"]}', body=body, + resource_group=self.resource_group, + headers={'AI-Tenant-Scope': 'true'}) + self.assertEqual(response_dict['message'], br.message) + + def test_delete_secret(self): + test_secret_name = 'test_secret_name' + response_dict = {'id': test_secret_name, 'message': 'The secret has been removed'} + self.rest_client_mock.delete.return_value = response_dict + br = self.client.delete(name=test_secret_name, resource_group=self.resource_group) + self.rest_client_mock.delete.assert_called_with(path=f'{self.url_prefix}/{test_secret_name}', + resource_group=self.resource_group, + headers={'AI-Tenant-Scope': 'true'}) + self.assertEqual(response_dict['message'], br.message) diff --git a/packages/core/tests/tracking/__init__.py b/packages/core/tests/tracking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/core/tests/tracking/test_tracking.py b/packages/core/tests/tracking/test_tracking.py new file mode 100644 index 0000000..21c5992 --- /dev/null +++ b/packages/core/tests/tracking/test_tracking.py @@ -0,0 +1,291 @@ +import copy +import uuid +import os + +from unittest.mock import MagicMock +from unittest import mock + +from ai_api_client_sdk.models.metric import Metric +from ai_api_client_sdk.models.metric_custom_info import MetricCustomInfo +from ai_api_client_sdk.models.metric_resource import MetricResource +from ai_api_client_sdk.models.metric_tag import MetricTag + +from ai_core_sdk.tracking import Tracking +from ai_core_sdk.resource_clients.metrics_client import MetricsCoreClient +from ai_core_sdk.exception import AIAPIAuthenticatorException + +from ..resource_clients.resource_client_test_base import ResourceClientTestBase + +class TestTracking(ResourceClientTestBase): + def setUp(self): + n = 3 + self.rest_client_mock = MagicMock() + self.metric_resource_dicts = [self.create_metric_resource_dict() for _ in range(n)] + self.rest_client_mock.get.return_value = {'resources': copy.deepcopy(self.metric_resource_dicts), 'count': n} + token_creator = MagicMock() + self.tracking = Tracking('http://test_url', token_creator=token_creator) + self.tracking.metrics_core_client = MetricsCoreClient(self.rest_client_mock) + + @staticmethod + def create_metric_resource_dict(): + return { + "execution_id": str(uuid.uuid4()), + "metrics": [ + { + "name": "Error Rate", + "value": 0.98, + "timestamp": "2021-03-29T12:58:05Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Confusion Matrix", + "value": "test_confusion_matrix" + } + ] + } + + @staticmethod + def __patch_metrics_body(execution_id): + patch_mb = { + "execution_id": execution_id, + "metrics": [ + { + "name": "Test Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + } + ] + } + ], + "tags": [ + { + "name": "Test Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Test Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + return patch_mb + + @staticmethod + def __patch_metrics_body_with_artifact_label(execution_id): + patch_mb = { + "execution_id": execution_id, + "metrics": [ + { + "name": "Test Error Rate", + "value": 0.98, + "timestamp": "2021-06-10T06:22:19Z", + "step": 2, + "labels": [ + { + "name": "group", + "value": "tree-82" + }, { + "name": "metrics.ai.sap.com/Artifact.name", + "value": "test_artifact" + } + ] + } + ], + "tags": [ + { + "name": "Test Artifact Group", + "value": "RFC-1" + } + ], + "custom_info": [ + { + "name": "Test Confusion Matrix", + "value": "[{'Predicted': 'False', 'Actual': 'False','value': 34}]" + } + ] + } + return patch_mb + + def test_tracking_modify_metrics(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = {'execution_id': metrics_patch_data.get('execution_id'), + 'metrics': [Metric.from_dict(md) for md in metrics_patch_data['metrics']], + 'tags': [MetricTag.from_dict(mtd) for mtd in metrics_patch_data['tags']], + 'custom_info': [MetricCustomInfo.from_dict(mcid) for mcid in metrics_patch_data['custom_info']]} + self.tracking.modify(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body=self.__patch_metrics_body(execution_id='test_execution_id'), + resource_group=self.resource_group) + + def test_tracking_log_metrics(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'metrics': [Metric.from_dict(md) for md in metrics_patch_data['metrics']], + 'artifact_name': 'test_artifact' + } + self.tracking.log_metrics(**body, resource_group=self.resource_group) + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'metrics':self.__patch_metrics_body_with_artifact_label(execution_id='test_execution_id')['metrics'] + }, + resource_group=self.resource_group) + + def test_tracking_set_custom_info(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'custom_info': [MetricCustomInfo.from_dict(md) for md in metrics_patch_data['custom_info']], + } + self.tracking.set_custom_info(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'custom_info':self.__patch_metrics_body(execution_id='test_execution_id')['custom_info'] + }, + resource_group=self.resource_group) + + def test_tracking_set_tags(self): + metrics_patch_data = self.__patch_metrics_body(execution_id='test_execution_id') + body = { + 'execution_id': metrics_patch_data.get('execution_id'), + 'tags': [MetricTag.from_dict(md) for md in metrics_patch_data['tags']], + } + self.tracking.set_tags(**body, resource_group=self.resource_group) + + self.rest_client_mock.patch.assert_called_with(path='/metrics', + body={ + 'execution_id': metrics_patch_data.get('execution_id'), + 'tags':self.__patch_metrics_body(execution_id='test_execution_id')['tags'] + }, + resource_group=self.resource_group) + + def assert_metric_resources(self, mr_dict: dict, mr: MetricResource): + if 'metrics' in mr_dict: + mr_dict['metrics'] = [Metric.from_dict(md) for md in mr_dict['metrics']] + if 'tags' in mr_dict: + mr_dict['tags'] = [MetricTag.from_dict(mtd) for mtd in mr_dict['tags']] + if 'custom_info' in mr_dict: + mr_dict['custom_info'] = [MetricCustomInfo.from_dict(mcid) for mcid in mr_dict['custom_info']] + + def test_tracking_query_metrics(self): + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id']} + + mqr = self.tracking.query(filter=params['$filter'], execution_ids=params['execution_ids'], + resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_tracking_query_metrics_with_select(self): + select = 'metrics,tags' + select_list = select.split(',') + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id'], '$select': select} + + mqr = self.tracking.query(filter=params['$filter'], execution_ids=params['execution_ids'], select=select_list, + resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + response_with_metrics_tags = [] + for execution_data in self.metric_resource_dicts: + new_execution_data = {} + new_execution_data['execution_id'] = execution_data['execution_id'] + for select in select_list: + new_execution_data[select] = execution_data[select] + response_with_metrics_tags.append(new_execution_data) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(response_with_metrics_tags, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_tracking_query_with_only_execution_ids(self): + params = {'execution_ids': ['test_exec_id']} + mqr = self.tracking.query(execution_ids=params['execution_ids'], resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=params, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_tracking_query_metrics_with_no_parameters(self): + mqr = self.tracking.query(resource_group=self.resource_group) + self.rest_client_mock.get.assert_called_with(path='/metrics', params=None, resource_group=self.resource_group) + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_tracking_delete_metrics(self): + execution_id = 'test_exec_id' + self.tracking.delete(execution_id=execution_id, resource_group=self.resource_group) + params = {'execution_id': execution_id} + self.rest_client_mock.delete.assert_called_with(path='/metrics', params=params, + resource_group=self.resource_group) + + def test_tracking_query_metrics_local(self): + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id']} + tracking_local = Tracking() + mqr = tracking_local.query(filter=params['$filter'], execution_ids=params['execution_ids'], + resource_group=self.resource_group) + params['execution_ids'] = ','.join(params['execution_ids']) + self.rest_client_mock.get.assert_not_called() + + def test_tracking_query_metrics_within_aicore(self): + params = {'$filter': 'test_filter', 'execution_ids': ['test_exec_id']} + response_payload = { + 'resources': copy.deepcopy(self.metric_resource_dicts), + 'count': len(self.metric_resource_dicts) + } + with mock.patch.dict( + os.environ, + { + 'AICORE_EXECUTION_ID': 'test_execution_id', + 'AICORE_TRACKING_ENDPOINT': 'https://mock/tracking', + 'AI-MAIN-TENANT': 'test_main_tenant', + 'AI-RESOURCE-GROUP': 'test_resource_group', + }, + clear=False, + ): + with mock.patch( + "ai_core_sdk.resource_clients.internal_rest_client.RestClient._handle_request", + return_value=response_payload, + ) as handle_request_mock: + tracking_within_aicore = Tracking() + mqr = tracking_within_aicore.query(filter=params['$filter'], execution_ids=params['execution_ids']) + expected_params = params.copy() + expected_params['execution_ids'] = ','.join(expected_params['execution_ids']) + handle_request_mock.assert_called_once() + call_kwargs = handle_request_mock.call_args.kwargs + self.assertEqual(call_kwargs['params'], expected_params) + forwarded_headers = call_kwargs['headers'] + self.assertEqual(forwarded_headers['AI-MAIN-TENANT'], 'test_main_tenant') + self.assertEqual(forwarded_headers['AI-RESOURCE-GROUP'], 'test_resource_group') + rest_client = tracking_within_aicore.metrics_core_client.rest_client + self.assertEqual(rest_client.tenant, 'test_main_tenant') + self.assertEqual(rest_client.resource_group, 'test_resource_group') + self.assert_object_lists(self.metric_resource_dicts, mqr.resources, self.assert_metric_resources, + sort_key='execution_id') + + def test_tracking_authenticator_exception(self): + with self.assertRaises(AIAPIAuthenticatorException): + self.tracking = Tracking('http://test_url') \ No newline at end of file diff --git a/packages/gen/AICORE.generative-ai-hub-sdk.png b/packages/gen/AICORE.generative-ai-hub-sdk.png new file mode 100644 index 0000000..d9fe35f Binary files /dev/null and b/packages/gen/AICORE.generative-ai-hub-sdk.png differ diff --git a/packages/gen/CONTRIBUTING.md b/packages/gen/CONTRIBUTING.md new file mode 100644 index 0000000..022ef7d --- /dev/null +++ b/packages/gen/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# How to Contribute + +This project is executed as [InnerSource](https://go.sap.corp/innersource) project. I.e., every individual or team within SAP is welcome to contribute, irrespective of the location or unit. To make your contribution a success, please take a look at the following guidelines. + +> **Note:** We also define a set of values and rules for effective collaboration in the [GOVERNANCE.md](GOVERNANCE.md) document. Please take a look to see how this project is run. + +There are different contribution types for you to choose from: + +- **Code**: you want to add features, bug-fixes or documentation to this project by adding code. +- **Time**: you want to support testing or work within our development team for a limited time, for example during a fellowship or internship. + +Check the corresponding sections below to get started. + +## Contributions + +### Code + +Please contribute code to this project via [pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). We recommend to align with the project team BEFORE you start working on the contribution. You can contact us via our [communication channels](SUPPORT.md#communication-channels). + +How to contribute code (features, bug-fixes or documentation): + + +1. Read the [definition-of-done](#definition-of-done) criteria and coding guidelines of this project +1. Work on your contribution and [get in touch with us](SUPPORT.md#communication-channels) in case you need support +1. Please follow the [commit convention](https://wiki.one.int.sap/wiki/display/AI/Continuous+Integration+and+Continuous+Deployment#ContinuousIntegrationandContinuousDeployment-CommitConventions) for automated versioning +1. After finalizing your contribution create a `pull request` against the `main branch` +1. In the pull request, document your changes including: + - Reason for contribution or change + - Description of feature and the use cases / solved requirements + - Determine suitable reviewers for your change +1. Your contribution will be reviewed (and hopefully approved) by the maintainers of the project team +1. We expect you to remain available to support your contribution for at least 30-days-warranty after its first shipment + +Alternatively, you can contact us requesting collaborator status, which will allow you to open PRs directly. + +Please have a look at [our github issues](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues) - corresponding contributions are welcome. If you are new to this project, look out for issues tagged as `good first issue`. + +### Time + +You will support our project by taking over testing tasks or you will join our development team for a limited time period and work under guidance of the product owner architect, and with direct access to the core repository. The same rules as for code contributions apply. Please [get in touch with us](SUPPORT.md#communication-channels) to arrange the details. + +## Definition-of-Done + +The Definition-of-Done criteria for this project are: + +- Unit tests and integration tests successfully run and documented. +- PR Build is fine. +- Pylint gives no errors. + +## Documentation + +As a contributor you are asked to document new features added by your code in [PYPIDESCRIPTION.md](PYPIDESCRIPTION.md). This document is published also as PyPi documentation. +Add documentation in the code as the generated [docs](docs/ai_core_llm_sdk.html) for developers rely on this documentation. + +**Thanks again for considering a contribution for this project.** diff --git a/packages/gen/GOVERNANCE.md b/packages/gen/GOVERNANCE.md new file mode 100644 index 0000000..6fc7f4e --- /dev/null +++ b/packages/gen/GOVERNANCE.md @@ -0,0 +1,52 @@ +# How this project is run + + The project team is fully responsible for: + +- the project roadmap +- timeline and priorities +- reviewing (approving or rejecting) pull requests +- public releases of the project + + The model we select for this GOVERNANCE.md is the [central responsibility model](https://github.tools.sap/innersource/innersource/blob/master/governance-models/models/central-responsibility.md). + +## Community Setup + +We are an [InnerSource](https://go.sap.corp/innersource) community and run this project with the spirit and methodology of open source software, but inside the company. Any use of the term **'community'** in this document refers to the key stakeholders - the project team, consumers, and contributors. + +## Values + +We define a set of core values for this project similar to successful open source communities, such as the [Cloud Native Computing Foundation](https://github.com/cncf/foundation/blob/master/charter.md) or the [Apache Foundation](https://www.apache.org/foundation/policies/conduct): + +- We invite **anyone within SAP** to participate in our community. Organizational boundaries shall be overcome. +- We **work together** to resolve conflicts, assume good intentions, and act in an empathetic fashion +- We run a **transparent decision-making process**, based on trust and alignment. +- **Quality is paramount**: Trust into our software builds slow and is destroyed quickly. Setup and processes ensure that quality is rated higher than speed. + +## Contributions + +Contributions are welcome. If you would like to contribute to this project, please have a look at our [contribution guidelines](CONTRIBUTING.md). + +## Roles and Responsibilities + +First, let's define some important roles: + +- **Contributor**: Anybody within SAP can become a contributor. A contributor is an individual or team who contributes to the community project, whether it is code, documentation or tests, but does not have access to merge code directly. + +- **Maintainer**: Maintainers review pull requests - they approve or reject changes in order to make sure that the overall vision, scope, architecture and roadmap of the project is kept. + +The overall responsibility for our project is with the project team. It governs the project and is responsible for +- the project scope, roadmap, backlog and prioritization of requirements and tasks (based on input of all stakeholders, especially contributors and consumers) +- the overall architecture of the project +- all maintainer tasks (see maintainer role above) + +As the project progresses this might change, and the project governance will be adjusted accordingly. + +## Documentation + +High quality documentation is as important as the shipped code. **Developer documentation** is handled as part of the code and **user documentation** is handled as a set of [markdown documents](docs/index.md). + +## Communication Channels + +Collaboration, creativity, and meaningful innovation depend on good communication. To make your contribution a success, be open and transparent about your planned development and closely align with the project team. Use communication tools that allow anyone to easily find discussions that happened in the past. + +To contact us or get in touch with our community, please have a look at our [communication channels](SUPPORT.md#communication-channels). \ No newline at end of file diff --git a/packages/gen/PYPIDESCRIPTION.md b/packages/gen/PYPIDESCRIPTION.md new file mode 100644 index 0000000..68d78c1 --- /dev/null +++ b/packages/gen/PYPIDESCRIPTION.md @@ -0,0 +1,22 @@ +# SAP Cloud SDK for AI (Python) - generative + +With this SDK you can leverage the power of generative models available in the generative AI Hub of SAP AI Core. +The SDK provides model access by wrapping the native SDKs of the model providers (OpenAI, Amazon, Google), through langchain, or through the orchestration service. + +## Installation + +To install this SDK, use the following pip command, which includes support for all models including langchain support: + + pip install "sap-ai-sdk-gen[all]" + +The default installation only includes OpenAI models (without langchain support): + + pip install sap-ai-sdk-gen + +You can install a subset of the extra libraries (without langchain support) by specifying them in square brackets: + + pip install "sap-ai-sdk-gen[google, amazon]" + +## Configuration, Usage + +Please refer to the official documentation hosted on [help.sap.com](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/index.html) for details on how to configure and use the SAP Cloud SDK for AI (Python). diff --git a/packages/gen/README.md b/packages/gen/README.md new file mode 100644 index 0000000..6b26b2c --- /dev/null +++ b/packages/gen/README.md @@ -0,0 +1,105 @@ +# SAP Cloud SDK for AI (Python) - generative + +The SDK formerly known as *generative AI Hub SDK* was rebranded. +With this SDK you can leverage the power of Large Language Models available in SAP's Generative AI Hub. + +## Installing and Using the SDK + +Use the new package name to install the SDK: + +```bash +pip install sap-ai-sdk-gen[all] +``` + +The class names have not changed i.e., you can continue to use existing code. + +> [!NOTE] +> Please refer to the [Generative AI Hub SDK Documentation](https://github.wdf.sap.corp/pages/AI/generative-ai-hub-sdk) + +### For SAP Internal Teams + +For internal SAP teams, the latest version of the SDK is available in our private artifactory. You will need to modify your pip.conf file to point to our internal repository. Here's how your pip.conf should look like : + +```conf +[global] +index-url = https://int.repositories.cloud.sap/artifactory/api/pypi/build-snapshots-pypi/simple +trusted-host = int.repositories.cloud.sap +``` + +This will ensure pip looks for packages in the SAP internal repository. + +After setting up the pip.conf file, you can install the SDK using the same pip command mentioned in the documentation. + +## Development + +The main modules are located in the subfolder [proxy](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/tree/main/gen_ai_hub/proxy): + +- [gen_ai_hub_proxy](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/tree/main/gen_ai_hub/proxy/gen_ai_hub_proxy) +- [langchain](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/tree/main/gen_ai_hub/proxy/langchain) +- [native](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/tree/main/gen_ai_hub/proxy/native) + +### Docstring Convention +In this project, we use the `reStructuredText` format for all Python docstrings, following the guidelines outlined in [PEP 257](https://peps.python.org/pep-0257/) and the [Sphinx documentation style guide](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html). +This ensures consistency and compatibility with other SDKs in the same namespace. + +Please follow these conventions when contributing to the codebase. + +## Renovate Setup + +Renovate is set up for this repository. For further information, take a look at the [documentation in ml-api-facade](https://github.wdf.sap.corp/AI/ml-api-facade/blob/master/docs/renovate.md). + +## Integration Tests + +This project utilizes integration tests to verify the system’s behavior across its various components. The tests are split into two primary groups: + +1. **Main Integration Tests:** Most of the integration tests are executed against the main cluster, ensuring that the major functionalities and interactions in the system behave as expected. + +2. **Bedrock Integration Tests:** A targeted subset of integration tests, known as the Bedrock tests, are executed in a separate cluster US10 (prod). These tests focus specifically on the Bedrock portions of the system and are annotated with `@pytest.mark.bedrock` within the test classes. This separation allows for targeted testing of Bedrock-specific features without interference from the broader system functionalities. + +### Running Bedrock Tests + +To facilitate the execution of the Bedrock integration tests, the `Makefile` includes the command `run-acceptance-test-us10`. This command specifically triggers the execution of the Bedrock tests, enabling the CI system to verify the Bedrock integrations in isolation (separate step). + +The mechanism of splitting the tests into distinct clusters and utilizing specialized commands for targeted testing helps in achieving more organized, efficient, and effective testing processes. + +Additional details regarding custom test stages and configuration specific to the cumulus can be found in the `customTestStages` section of the `config.yaml` in the `.pipeline` folder. + +## Documentation + +### Overview + +Our project includes an extensive internal documentation to assist developers and users in understanding the architecture, usage, and development of the project. The documentation is built automatically via our continuous integration workflows using [Sphinx](http://www.sphinx-doc.org/), a robust documentation framework that converts [reStructuredText](http://docutils.sourceforge.net/rst.html) files into various output formats. + +### Building Documentation Locally + +If you wish to build the documentation on your local machine, follow these steps: + +1. Navigate to the docs/ directory of the project. +2. Make sure you have Sphinx installed. If not, install it using `pip install sphinx` or `pip install -r requirements.txt` +3. Build the documentation by running `make preview_html`. This command will generate HTML output for the documentation and allow you to preview it in a web browser. + +### Automated Documentation Builds with GitHub Actions + +Our project leverages GitHub Actions to automate the documentation building process. The workflow is defined in `.github/workflows/documentation.yml`. It is triggered each time a pull request (PR) is merged into the `main` branch. Here's a rough sequence of the automated process: + +1. GitHub Actions selects several important files within the project required for generating the documentation. +2. Using the Sphinx framework, GitHub Actions compiles these files into HTML format. +3. Upon successful build, the compiled documentation is pushed to the `gh-pages` branch of the repository. + +### Documentation Location + +Once the documentation is built by GitHub Actions, it is hosted and available for viewing at the GitHub Pages site. You can access the latest version of the internal documentation at the following URL: + +[SAP Cloud SDK for AI (Python) - generative](https://github.wdf.sap.corp/pages/AI/generative-ai-hub-sdk/) + +Note that the content on this site reflects the most recent documentation build from the `gh-pages` branch. + +### Contributing to this Project + +This project is Innersource and if you wish to contribute to this project please request write access to the repository by requesting CAM profile `AI Github AI SDK-Contributors`. More details on contributing to this project can be found in the [CONTRIBUTING.md](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/blob/main/CONTRIBUTING.md) file. + +## Kudos + +The code for this SDK is originated from [llm-commons](https://github.tools.sap/AI-Playground-Projects/llm-commons). + +Kudos to the authors (especially Mathis BÃļrner). \ No newline at end of file diff --git a/packages/gen/README_sphynx.md b/packages/gen/README_sphynx.md new file mode 100644 index 0000000..5bfec28 --- /dev/null +++ b/packages/gen/README_sphynx.md @@ -0,0 +1,250 @@ +# SAP Cloud SDK for AI (Python) - generative + +The SDK formerly known as *generative AI Hub SDK* was rebranded. + +With this SDK you can leverage the power of generative models available in the generative AI Hub of SAP AI Core. +This SDK provides LLM access by wrapping the native SDKs of the model providers (OpenAI, Amazon, Google), +through langchain, or through the orchestration service. + +(installation)= +## Installation + +Use the package name to install the SDK with support for all models (OpenAI, Amazon, Google) +including langchain support: + +```bash +pip install "sap-ai-sdk-gen[all]" +``` + +With the name rebranding, class names have **not** changed i.e., you can continue to use existing code. + +The default installation only includes OpenAI models (with langchain support): + +```bash +pip install sap-ai-sdk-gen +``` + +You can install a subset of the extra libraries (with langchain support) by specifying them in square brackets: + +```bash +pip install "sap-ai-sdk-gen[google, amazon]" +``` + +In the table below, you can see which models and vendor specific langchain packages are installed when using different installation parameters. + +| Install Parameter | OpenAI | Google | AWS | LangChain | OpenAI-LangChain | Google-LangChain | AWS-LangChain | +|-------------------------------------|--------|--------|-----|-----------|------------------|------------------|---------------| +| | yes | no | no | yes | yes | no | no | +| [google] | yes | yes | no | yes | yes | yes | no | +| [amazon] | yes | no | yes | yes | yes | no | yes | +| [amazon, google] / [google, amazon] | yes | yes | yes | yes | yes | yes | yes | +| [all] | yes | yes | yes | yes | yes | yes | yes | + +## Configuration + +There are different ways to configure the SAP AI Core access (listed in order of precedence): + +- environment variables +- (profile) configuration file +- from VCAP_SERVICES environment variable, if it exists + +These methods automatically initialize an authenticated client. +For custom authentication, you can provide a `proxy_client` parameter when instantiating SDK classes to use your own +`GenAIHubProxyClient` with direct credential configuration. + +We recommend setting these values as environment variables or via config file. The default path for the configuration file +is `~/.aicore/config.json` + +### Environment variables + +- `AICORE_CLIENT_ID`: This represents the client ID. +- `AICORE_CLIENT_SECRET`: This stands for the client secret. +- `AICORE_AUTH_URL`: This is the URL used to retrieve a token using the client ID and secret. +- `AICORE_BASE_URL`: This is the URL of the service (with suffix /v2). +- `AICORE_RESOURCE_GROUP`: This represents the resource group that should be used. +- `AI_CLIENT_TYPE` (optional): Specify client type in request headers. Default is 'GenAI Hub SDK (Python)'. Note: This cannot be set in the config file. + +For using X.509 credentials, you can set the file paths to certificate and key files, or certificate and key strings, +as an alternative to client secret. + +- `AICORE_CERT_FILE_PATH`: This is the path to the file which holds the X.509 certificate +- `AICORE_KEY_FILE_PATH`: This is the path to the file which holds the X.509 key +- `AICORE_CERT_STR`: This is the content of the X.509 certificate as a string +- `AICORE_KEY_STR`: This is the content of the X.509 key as a string + +### Configuration files + +By default, the configuration file is located at `~/.aicore/config.json`. You can change the directory where the config file is located by setting the `AICORE_HOME` environment variable. + +Note: tilde (~) is not supported, so use the full path to the directory. + +A profile is a json file residing in a config directory. With profile names one can switch easily between profiles e.g., for different (sub)accounts. The profile name can be passed also as a keyword. If no profile is specified, the default profile is used. Specify the profile via envionment variable `AICORE_PROFILE`. The associated configuration file then needs to have file name `config_{profile}.json` + +The command `aicore configure --help` can be used to generate a profile. + +The following list explains which environment variables can be used to control which configuration file will be used: + +1. **`AICORE_HOME`**: This variable represents a directory path. Within this directory, various configuration files can be stored and the SDK will automatically load them from there based on the "AICORE_PROFILE" environment variable. + +2. **`AICORE_PROFILE`**: This variable allows users to switch between different configurations stored in the `AICORE_HOME` directory. It is important to note that `AICORE_PROFILE` does not represent the complete name of a configuration file. Instead, it refers to a profile name, which corresponds to a file named `config_{profile}.json`. If AICORE_PROFILE is empty `$AICORE_HOME/config.json` is used. + +3. **`AICORE_CONFIG`**: This variable overrides both `AICORE_HOME` and `AICORE_PROFILE`. It specifies the direct absolute path to a configuration file that will be used. + +The configuration file should be: + +```json +{ + "AICORE_AUTH_URL": "https://* * * .authentication.sap.hana.ondemand.com/oauth/token", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CLIENT_SECRET": "* * * ", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +or + +```json +{ + "AICORE_AUTH_URL": "https://* * * .authentication.cert.sap.hana.ondemand.com", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CERT_FILE_PATH": "* * */cert.pem", + "AICORE_KEY_FILE_PATH": "* * */key.pem", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +or + +```json +{ + "AICORE_AUTH_URL": "https://* * * .authentication.cert.sap.hana.ondemand.com", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CERT_STR": "* * *", + "AICORE_KEY_STR": "* * *", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +## Usage + +### Prerequisite + +For direct model access, you need to create a deployment for each desired model according to according to +the [help documentation for model deployments](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core). + +For model access through the orchestration service, you need to create a deployment of the orchestration service according to the [help documentation for orchestration service deployments](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-orchestration) + +### Examples + +In section "*Examples*" there are code snippets for each Large Language and Embedding model as well as for the orchestration service usage. + +(supported_models)= +## Supported Models + +The list of models in the Generative AI Hub of SAP AI Core can be found in [SAP note 343776](https://me.sap.com/notes/3437766). +Among these, the following models are currently supported in the SAP Cloud SDK for AI (Python) - generative: + +### LLM Models + +| Provider | Model Name | Streaming Support | +|------------|------------------------------------|-------------------| +| Amazon | amazon--nova-lite | No | +| | amazon--nova-micro | No | +| | amazon--nova-pro | No | +| | amazon--amazon--nova-premier | Yes | +| Anthropic | anthropic--claude-3-haiku | Yes | +| | anthropic--claude-3.5-sonnet | Yes | +| | anthropic--claude-3.7-sonnet | Yes | +| | anthropic--claude-4-sonnet | Yes | +| | anthropic--claude-4-opus | Yes | +| | anthropic--claude-4.5-sonnet | Yes | +| | anthropic--claude-4.5-haiku | Yes | +| | anthropic--claude-4.6-sonnet | Yes | +| | anthropic--claude-4.6-opus | Yes | +| Google | gemini-2.0-flash | Yes | +| | gemini-2.0-flash-lite | Yes | +| | gemini-2.5-flash | Yes | +| | gemini-2.5-pro | Yes | +| | gemini-2.5-flash-lite | Yes | +| MistralAI | mistralai--mistral-small-instruct | No | +| | mistralai--mistral-medium-instruct | No | +| | mistralai--mistral-large-instruct | No | +| OpenAI | gpt-4o | Yes | +| | gpt-4o-mini | Yes | +| | gpt-4.1 | Yes | +| | gpt-4.1-mini | Yes | +| | gpt-4.1-nano | Yes | +| | gpt-5 | Yes | +| | gpt-5-mini | Yes | +| | gpt-5-nano | Yes | +| | gpt-5.2 | Yes | +| | gpt-5.3-codex | Yes | +| | gpt-5.4 | Yes | +| | gpt-5.4-nano | Yes | +| | o1 | No | +| | o3 | Yes | +| | o3-mini | No | +| | o4-mini | Yes | +| Cohere | cohere--command-a-reasoning | Yes | +| | cohere--reranker | Yes | +| Perplexity | sonar | Yes | +| | sonar-pro | Yes | +| | sonar-deep-research | Yes | + +### Embedding Models + +| Provider | Model Name | +|----------|---------------------------------| +| Amazon | amazon--titan-embed-text | +| | amazon--titan-embed-image | +| Google | google--gemini-embedding | +| NVIDIA | nvidia--llama-3.2-nv-embedqa-1b | +| OpenAI | text-embedding-3-small | +| | text-embedding-3-large | +| | text-embedding-ada-002 | + +### Notes on model usage + +- âš ī¸ **Anthropic & Amazon**: + - Currently, for `amazon--nova-lite`, `amazon--nova-micro`, and `amazon--nova-pro`, the supported method is `converse`. `invoke` and `invoke_model_with_response_stream` are not supported. +- â„šī¸ **MistralAI:** + - This model only supports the following roles in the order implied: user/assistant/user/assistant/.... +- â„šī¸ **Perplexity:** + - The Perplexity Sonar models are also based on the OpenAI SDK and usage for these models is similar to that of GPT models. + - Search-API is not supported yet. +- â„šī¸ **Cohere:** + - The cohere--command-a-reasoning model is also based on the OpenAI SDK and usage for this model is similar to that of GPT models. +- **Models not added to SDK yet**: + - You can also try using Generative AI Hub SDK for models that are already in Generative AI Hub, but not supported yet + by the SDK. This can be done by additionally specifying the model initialization: see [](unsupported_models). + Please note, that it's not guaranteed that it will work. Because there might be some new models, for which customization in the SDK + is needed. + +(package_dependencies)= +## Package dependencies + +Please note the following dependencies of sap-ai-sdk-gen: + +```text +httpx>=0.27.0 +h11>=0.16.0 +dacite>=1.8.1 +click>=8.1.7 +overloading==0.5.0 +packaging>=23.2 +sap-ai-sdk-core>=3.1.0 +pydantic~=2.12 +openai>=1.58.1 +google-genai~=1.60.0 # google +boto3>=1.40.61 # amazon +aiobotocore>=3.0.0 # amazon +langchain~=1.2.6 +langchain_google_genai~=4.2.0 # google +langchain-classic~=1.0.0 +langchain-community~=0.4.1 +langchain-openai~=1.1.0 +langchain-aws~=1.1.0 # amazon +``` diff --git a/packages/gen/RELEASE_NOTES.md b/packages/gen/RELEASE_NOTES.md new file mode 100644 index 0000000..7da08e0 --- /dev/null +++ b/packages/gen/RELEASE_NOTES.md @@ -0,0 +1,242 @@ +# Release Notes +## 6.10.0 + +### Features +- Added model_version as model identifier +- Updated Prompt Registry Client to align with API changes +- Added support for OpenAI Responses API, see [](responses_api) +- Enabled flat import for Model/Client classes +- Added support for newly added models to the AI Core, see [](supported_models) + +### Bugfixes +- Upgraded google-genai +- Upgraded langchain + +## 6.7.0 + +### Features +- Added support for Orchestration Config API of Prompt Registry, see [](orchestration_config_api) +- Added retry logic for token retrieval + +### Bugfixes +- Upgraded langchain-aws + +## 6.6.0 + +### Bugfixes +- Replaced the lingua-language-detector library, which was causing version conflict issues. + +## 6.5.0 + +### Features +- Enabled providing additional headers for Grounding Clients (Pipeline API Client, Retrieval API Client & Vector API Client) +- Added support for Orchestration V2 API /embeddings endpoint, see [](orchestration2) +- Added Evaluations Client, see [](evaluations) +- Added support for RPT-1 models, see [](rpt_models) + +### Bugfixes +- Fixed the issue regarding the handling of large streaming responses in AsyncSSEClient +- Upgraded langchain version +- Adjusted to the changes in Orchestration V2 API +- Removed aiboto3 dependency in favor of aibotocore + +## 6.1.2 + +### Breaking Changes + +- Switch to [langchain 1.x](https://docs.langchain.com/oss/python/langchain/overview) This also results in upgrading of dependent langchain libraries. You need to ensure your code works with the upgraded dependencies. Langchain 0.3.x is no longer supported in the SDK. Please refer to [](package_dependencies) for details on the dependencies. +- Switch from langchain-google-vertexai to langchain-google-genai and google-cloud-aiplatform to google-genai due to [deprecation of these libraries](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/deprecations/genai-vertexai-sdk) + +### Features +- Added support for additional embedding models: Amazon Titan Embedding, Llama 3.2 Embedding, Google Gemini Embedding + +## 5.11.0 + +### Features +- Added support for orchestration V2 API. For details, see [](orchestration2). +- Added API reference documentation +- Added support for new models: Claude 4.5 Sonnet, Claude 4.5 Haiku, Cohere Command-a-reasoning, Cohere reranker, Gemini 2.5 Flash Lite, Perplexity Sonar, Perplexity Sonar-Pro, Mistral Medium +- Removed decomissioned models: IBM granite 13b, Meta Llama 3.1, Claude 3 Opus + +## 5.10.0 + +### Features + +- Added retry logic for orchestration service with exponential backoff. Use method "run_with_retries" instead of "run" for your orchestration service instance. + +## 5.9.0 + +### Bugfixes + +- Upgraded boto3 and langchain-aws dependencies and relaxed the dependecy to pydantic libary, see [](package_dependencies) for details. + +## 5.8.0 + +### Features + +- Added additional apis for document grounding: vector api and retrieval api support and additional methods for pipelines api. + +## 5.7.5 + +### Features + +- Added support for new models: Amazon Nova Premier, Claude 4 Opus, Gemini 2.5-flash, Gemini 2.5-pro, GPT-5, GPT-5-mini, GPT-5-nano, Mistral Small Instruct. See [](supported_models) for a comprehensive overview of supported models. +- Removed old models: Amazon Titan Text Express/Lite, Gemini 1.5-flash, Gemini 1.5-pro, Claude 3 Sonnet +- Allow botocore.config as input for Amazon Bedrock to set additional parameters, e.g. connect_timeout + +### Bugfixes + +- Upgraded langchain-google-vertexai to fix a [bug in ChatVertexAI](https://github.com/langchain-ai/langchain-google/issues/1057) +- Upgraded boto3 and langchain-aws dependencies to allow tool binding with Claude 4 + +## 5.6.3 + +### Features + +- Added support for converse_stream for aws models and event streams + +### Bugfixes + +- Display deployment not found error in orchestration +- Return http headers from orchestration in case of error + +## 5.5.0 + +### Features + +- Added support for Claude 4 Sonnet + +### Bugfixes + +- Upgraded langchain-google-vertexai to fix a bug with streaming on newer gemini models +- Fix issue where template input parameters would be converted to CamelCase when sending to prompt registry API + +## 5.4.5 + +### Features + +- Added support for additional storage types (S3/SFTP) for the Grounding module of the Orchestration Service. + + +## 5.4.1 + +### Features + +- Added support for images in the orchestration service. See [](input_images) for details. +- Added timeout parameter for amazon native streaming calls in method `invoke_model_with_response_stream`. + +## 5.3.4 + +### Breaking Changes + +- Switch to different distribution name as part of a rebranding to `SAP Cloud SDK for AI (Python) - generative`. See [](installation) for details. The package and subpackage names are unchanged, therefore no code adjustment is necessary, only the installation of the SDK has changed. + +### Features + +- Added support for the translation module in the orchestration service. See [](translation) for details. +- Added support for function calling in the orchestration service. See [](tool_calling) for details. +- Added support for OpenAI o3, o4-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano models. +- Reworked the langchain dependency chain, only installing the vendor specific langchain libraries on demand, see [](installation) for details. + +### Bugfixes + +- Set the dependency to h11 library and relaxed the dependecy to pydantic libary, see [](package_dependencies) for details. + +## 4.12.1 + +### Features +- Added support for Anthropic Claude 3.7 Sonnet model. +- Added support for gemini-2.0 and gemini-2.0-flash models. +- Retirement of Gemini 1.0 Pro. +- Added async examples for bedrock and vertex models. See [](async_examples) for details. +- Deprecation of SAP Generative AI Hub SDK, as it will be rebranded to sap-ai-sdk-gen. + +## 4.10.2 + +### Features + +- Added support for OpenAI o1 and o3-mini models. +- Added support for AWS amazon--nova-micro, amazon--nova-lite, and amazon--nova-pro models. +- Added support for asynchronous calls to Bedrock models. +- Added support for asynchronous calls to Vertex models. +- Added support for `masked_grounding_input` and `allowlist` also for the grounding output in the orchestration service. See [](allow_list) for details. +- Deprecation of `input_filters` and `output_filters` in the orchestration configuration, use `ContentFiltering` instead. See [](content_filtering) for details. + +## 4.4.3 + +### Features + +- Add support for LlamaGuard38b content filtering in the orchestration service. You can use LlamaGuard38b filters for filtering input and output along different content categories, see [SAP AI Core help documentation](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/input-filtering?locale=en-US). For usage examples, see [](content_filtering) +- Add support for grounding metadata parameters, see [SAP AI Core help documentation](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/metadata?locale=en-US) +- Add support for asynchronous calls to orchestration service. See [](orchestration_async) for details. + +## 4.3.1 + +### Features + +- Add support for prompt registry APIs. You can create, retrieve and modify prompt templates from the prompt repository. For example usage, see [](prompt_registry_notebook) +- Add support for grounding in orchestration. You can now configure the grounding module in the orchestration service. +- Add support for structured output in the orchestration service by specifying the response format, for instance text or json. See [](response_format) for details. +- Add autodiscovery for orchestration deployments. See [](orchestration_deployment) for details. + +### Bugfixes + +- OpenAI deprecated max_tokens in favor of max_completion_tokens parameter. This was now also included in the generative AI Hub SDK and the dependency of the langchain-openai version could be relaxed. + +## 4.1.1 + +### Features + +- Add support for prompt registry templates in orchestration. You can now configure a prompt registry template in the orchestration service call by referencing the ID or scenario, template name, and version. See [](prompt_registry) + +### Bugfixes + +- Set langchain-openai==0.2.9 due to max_completion_token issues with later versions. + +## 4.0.0 + +### Breaking Changes + +- Switch to [langchain 0.3.x](https://python.langchain.com/docs/versions/v0_3/) This also results in upgrading of dependent langchain libraries and a transition to pydantic v2. You need to ensure your code works with the upgraded dependencies. Langchain 0.2.x is no longer supported in the SDK. Please refer to [](package_dependencies) for details on the dependencies. + +### Features + +- Add support for streaming in orchestration service. See the example notebook here: [](orchestration_streaming). +- Add enhanced debug logging: When log level debug is enabled the source of configuration will be logged to support troubleshooting. + +## 3.8.0 + +### Features + +- Add support for mistralai--mistral-large-instruct model +- Add support for ibm--granite-13b-chat model +- Add capability to access unsupported models, see the example notebook [](unsupported_models) for details. +- Add enhanced logging for API calls + - By setting the environment variable `DEBUG_LOG_API_CALLS` to `true`, all calls to the backend are logged for better error diagnosis + +## 3.2.6 + +### Features + +- Add support for orchestration service: data masking. See the example notebook section [](content_filtering) for details. + +### Bugfixes + +- Bugfix for x509 certificate authentication support + +## 3.1.1 + +### Features + +- Add support for gpt-4o model + +## 3.1.0 + +### Breaking Changes + +- Switch to vertexAI SDK for native Google model access. The previous library 'google-generativeai' is no longer supported by the generative AI Hub SDK. + +### Features + +- Add support for orchestration service: templating, content safety, inference. See the example notebook [](orchestration) for details. +- Add support for anthropic--claude-3.5-sonnet model diff --git a/packages/gen/SUPPORT.md b/packages/gen/SUPPORT.md new file mode 100644 index 0000000..040007e --- /dev/null +++ b/packages/gen/SUPPORT.md @@ -0,0 +1,20 @@ +# How to get support + +## Usage Notes + +The purpose of a SUPPORT.md file is to provide information how consumers of your project and other interested parties can get support. + +The preferred way to submit support requests is to reach out to the project team and the project community via the communication channels listed below. + +In case you want to report a bug, please create an issue. + +Contact us in case of any questions and let us know if we can help you with anything! + +## Communication Channels + +Within the project we are using these communication channels: + +- Issues: [GitHub](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues) +- Chat: [Slack](#ai-core-sdk) +- Team: [Contributors](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/graphs/contributors) +- Planning: [Jira](https://jira.tools.sap/browse/AI-33952) \ No newline at end of file diff --git a/packages/gen/docs/CHANGELOG.md b/packages/gen/docs/CHANGELOG.md new file mode 100644 index 0000000..5303244 --- /dev/null +++ b/packages/gen/docs/CHANGELOG.md @@ -0,0 +1,2005 @@ +# [7.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.10.0...v7.0.0) (2026-04-28) + + +### Bug Fixes + +* **version:** dummy commit ([33de71e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/33de71ea2fd54f131fff43c90eda024c1cf6e477)) + + +### BREAKING CHANGES + +* **version:** bump major version + +# [6.10.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.9.1...v6.10.0) (2026-04-27) + + +### Bug Fixes + +* **tests:** clean up test models ([36d1671](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/36d1671d3d73684fd05c69b22e43b11990469895)) +* **tests:** fix langchain amazon tests ([c984dc9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c984dc9b5277d047cc3b4d812656d43043866a3d)) + + +### Features + +* **proxy:** add GPT-5.2, Perplexity Deep Research, Claude Sonnet 4.6 and Claude Opus 4.6 models ([aed7389](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/aed7389bc90f1c0e422042c935c62098ad5a4176)) +* **proxy:** add gpt-5.3-codex, gpt-5.4, gpt-5.4-nano models ([8eb6ec6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8eb6ec6800fcd5acf4bc89c15204e9c20e1ee2e0)) +* **proxy:** fix after review ([ae8c332](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ae8c33211e292370e36800859c013d9f0b24cc6e)) +* **proxy:** increasing default timeout for setting up bedrock models ([170a44d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/170a44dfd9fc64ca0d99db973f489d3a6d7a9cc2)) +* **proxy:** update langchain aws model name convertion to model id ([0a1917c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0a1917c18fe14aa6c5b0e44d14b031324f3db935)) + +## [6.9.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.9.0...v6.9.1) (2026-04-09) + + +### Bug Fixes + +* **deps:** update dependency google-genai to ~=1.70.0 ([462eff3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/462eff3c8abc92db11f6507ef201a0563961ed58)) + +# [6.9.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.8.0...v6.9.0) (2026-04-09) + + +### Bug Fixes + +* **langchain:** upgrade langchain ([97a09eb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/97a09eb82f26a66b591eda6aa9218ff74aa809bf)) + + +### Features + +* **prompt registry:** add response format and tools to template spec ([2690c4b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2690c4b1d2ecfd99c285261fecfc876b3e9e3f9a)) +* **prompt registry:** updated error handling in content validation, add test for content validation ([a936a21](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a936a212f5c591a5cca2dd876641780a4326ba33)) +* **proxy:** add model version as deployment search param ([21f9369](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/21f9369e4161037800da45d99a8f26201c8bc6f3)) +* **proxy:** add responses functionality to open ai native client ([f9605e0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f9605e0faa21dc6d1918b3d2407bce75fd2c17cd)) +* **proxy.native.google_genai:** update google_genai version up to the latest, fix native client for google_genai so it works correctly with the new library version ([a1cdc85](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a1cdc853768a03b668a596701a7e2d0e98a67aa4)) + +# [6.8.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.7.0...v6.8.0) (2026-03-24) + + +### Bug Fixes + +* **tests:** fix integration test ([e204ef7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e204ef7c9b5b423825919ddc5c853822330145dc)) +* **tests:** fix some integration tests ([479a120](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/479a1206464abcc8c8e5d99912f072cd3601f70f)) + + +### Features + +* **document grounding:** flat import ([cc343fa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cc343fa4d15968683aaad0bce110ee29c002c1e8)) +* **evaluation:** fix after review ([96605e0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/96605e0ebbcbad6427ed1628df9c2c3f7a4a2bb5)) +* **evaluation:** fix after review ([4c5d826](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4c5d82675a816f79307c9b3a879ddfb323510777)) +* **evaluation:** flat import ([d0f6850](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d0f6850ee55a4ba6ee8720e5e7616cfe3ca8fd59)) +* **evaluation:** sonar cpd exclusion ([f46c9b9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f46c9b9681275881f05e4ced13e9f257d210a921)) +* **orchestration:** flat import ([dd0a0a2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dd0a0a2b27818877b8f8f8490e73366c06f32293)) +* **prompt registry:** flat import ([ab2657b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ab2657b94609a314c44810b77283116063f6b1a4)) + +# [6.7.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.6.2...v6.7.0) (2026-03-19) + + +### Bug Fixes + +* **deps:** update actions/setup-python action to v6 ([f3647f2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f3647f2efc68dda632ded005fb1f10cffc2aebb7)) +* **deps:** update dependency langchain-aws to ~=1.4.0 ([a3c061a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a3c061ac80c1935887c7e8210d58dfcd20fc6055)) + + +### Features + +* **prompt_registry:** add docs about orchestration config template functionality, updare requirements.txt ([b635c68](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b635c689b353909aa1747a7e445012104573ea7a)) +* **prompt_registry:** add orchestration config template functionality ([2cffb38](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2cffb38880ed6039cb2b76958784a46dd6ed8b97)) +* **prompt_registry:** fix after review ([bdfe7c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bdfe7c1fdd1e6c8d07d65f867e93a2f14ddaad24)) +* **prompt_registry:** fix naming ([cb91f47](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cb91f4763de20334d4622e87ee07d246af56b786)) +* **prompt_registry:** fix naming ([0c81a00](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0c81a005282a8163fd110f7200112d7ce267f451)) +* **prompt_registry:** fix unit tests ([d18faf8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d18faf8fec91eefeaac0b783cec8e72d768d0a53)) +* **prompt_registry:** not convert query params to camel case ([475a344](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/475a3444394f820aff70205db17f3a4996729583)) + +## [6.6.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.6.1...v6.6.2) (2026-03-11) + + +### Bug Fixes + +* missing register in langchain ([6d0a510](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6d0a5102d73374da2699384f6a3bea6c504582a8)) +* upgrade integration test model ([60724a8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/60724a8441f8c581b5a42aa60f665aedde6708ac)) +* upgrade to use gemini 2.5 flash lite model in the integration tests ([4cd600c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4cd600cf840d5f4ffa4c6e08f9829286e887fa79)) + +## [6.6.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.6.0...v6.6.1) (2026-03-10) + + +### Bug Fixes + +* **requirements:** upgrade sap-ai-sdk-core ([980037f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/980037fd403c431fefbcc1e2499bf4519e870a0d)) + +# [6.6.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.5.0...v6.6.0) (2026-03-04) + + +### Bug Fixes + +* fix sonar issues ([d19e69f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d19e69f9ee4f7bbd10e0b5719a944c93cccdec4b)) +* fixture naming, add retry on another test ([9b919fc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b919fc15d1944666d5521d98247187f845966b2)) +* lingua library ([6a9c482](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6a9c482ab56b9e736b1de7cc3127f95338c44f3d)) +* requirements ([6b6877a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6b6877ab579075da256c0514b8e61dd03b062035)) +* review comments ([cc30708](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cc30708c9c4e22dabde99239c8338a49a59cd7d1)) +* softened requirement for langcode ([429e065](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/429e0656b1982919a2e4d11f273cb6889e2e5a26)) +* **tests:** add retry decorator ([4e3866e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4e3866ee9a734fde0a759d2cbe641b9ab4e4f7c7)) + + +### Features + +* **tests:** skip instead of fail tests that do not pass due to rate limit issues ([950208e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/950208e310c714f77885249c1dec761bc8ed8381)) + +# [6.5.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.4.0...v6.5.0) (2026-02-25) + + +### Bug Fixes + +* **clients:** add proper cleanup, add unit tests ([129b1bf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/129b1bf322bf8a8ac2cd74f6f0b2de3b23b4cfb7)) +* ensure client cleanup ([35445aa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/35445aa9ac4eafbdf93760651a277761aedb419e)) +* **clients:** adjust amazon client to new library ([134ac3e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/134ac3e742a28d0aa30176a95e0629164a3cd559)) +* requirements ([421c784](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/421c784e3ce7da0eaca729bd3730b1fbe1a5509e)) +* **sonar:** minor code style fixes ([dcacefb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dcacefb8f88c74e9cdc6a6068194d3f1d5e62fd8)) + + +### Features + +* **dependencies:** remove aioboto3 ([8682858](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8682858da39230c79afe29b9c4dff40a5b4ec778)) + +# [6.4.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.3.0...v6.4.0) (2026-02-24) + + +### Bug Fixes + +* regex failure ([17a49cb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/17a49cb694bc9a7cbc561c9ea6bcee96566aa083)) +* acc tests ([d4d1ca1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d4d1ca185098f5baf9e03da42bdbc797677bb80f)) +* added few more validations and some bug fixes ([15d267b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/15d267b912a5d6c7a0faa91e29f1dc3485d5f1b4)) +* added lingua back ([3ef64f6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3ef64f6aad325f2007a505def49a511f647ed1b3)) +* added more unit tests ([9b64b01](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b64b0117fae41a8ec090336afa3a772ca1f9cb2)) +* added more unit tests ([b67f3f8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b67f3f884faf453e4919b9398ddfd3e78b1cc600)) +* added unit tests ([4d9ab79](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4d9ab791ff8f03d5d7e3c909da130746529f7c98)) +* cog com ([ccdc70a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ccdc70a1eab4b81289025718062f237c0998779d)) +* cognitive complexity ([d6c5d66](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d6c5d66be40533aabcb8cda753fc5e1e9af29626)) +* convert to unitTest from pytest ([79c8f6e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/79c8f6e7af7c0b6d62ac04dbfdde4bfc7568dae6)) +* coverage ([5442212](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5442212a608d2ba4cb9ad6e810fef2bd11c0bcd9)) +* credentials file updated with latest ai core sdk ([25fcebd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/25fcebda28ddc21fb2e1ea448dd7da2780787d04)) +* enable all stages in pipeline ([907d0a8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/907d0a8d5e086ff97d8bcc065d827e9363525a02)) +* enabled all other tests ([83934ef](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/83934ef647aa55bc0c0d99c93070c6fdc9e35e95)) +* gcc ([137b132](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/137b1320aa5064526aaa7a8faeac370fe3ee38b5)) +* happy path fixes and some refactor ([8990d87](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8990d87451310d76657770ce216b7222c7348223)) +* import cleanup ([a0fbf8b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a0fbf8b5616fdf6e3900c90025ca5b7a6dd2c759)) +* init test val ([493c8f6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/493c8f6c7b5ddb1071f3b75562df7434688522c8)) +* integration test ([872364b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/872364b8b3f663ddc2549925cb1716772f2d1552)) +* integration tests ([22a1ce2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/22a1ce2ec201c9b170a50c16d8539aea29f2ef62)) +* lingua version ([21ecd8e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/21ecd8e1f088bf32e8eba2bd271e5bc1bfae959c)) +* minor fixes ([4a98337](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4a9833752354261175175a83a0f9e9ff5aea89cc)) +* numpy version issue ([bdde104](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bdde104ff6885ad417063780d4d5c6393c87ac89)) +* orchv2 and other comments ([bdb8236](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bdb8236e1a48dba8388c201bad6f34929a5b1218)) +* print removed ([fca714c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fca714c9340f3ef8477103b67c5621f4a1632b7e)) +* pylint ([57ef818](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/57ef8180fd8c86010ee2f953f0028ef79f97791a)) +* pylint ([3eadab1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3eadab166ea0028feb1c2ac8cd8bba93ca0e41c3)) +* pylint ([f74854e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f74854eaeb4f4bfe24f9f3ee6ffe3fb65266bdf8)) +* pylint ([e01a5db](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e01a5dbb5ba725977a2d394e65578ef1123ca2d6)) +* pylint revert ([e86e938](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e86e938237a9e71fe8548175abb9de402c249d08)) +* refactor for building s3 file key and fix related to reading orch data from artifact ([5670640](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5670640d8beabbd230f614067f80e28d0c477655)) +* remove numpy ([c7afb33](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c7afb33e2a435aea25ff717146a78d5bf9173c8b)) +* remove some functions and fixes for completions and metrics results via run ([a941011](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a941011f4b9f1b3944fac64192211e46bc2acee8)) +* reuse orch v2 ([c3f4974](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c3f49749b4587fa0d723b333d51dda813ce25b98)) +* review comments ([a90d39c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a90d39cfe07a8023d6e4657a39748619dde6f997)) +* review comments ([69b4b9f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/69b4b9f4408dae9cceed1527857a75f37c6bb4a4)) +* review comments ([938dcfe](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/938dcfe371df11de7c54c77d1dc7371dec9f3a15)) +* template and orch registry in multi-exec ([77ae893](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/77ae893bbe1dacb38933e112a338cb777795df5d)) +* test with 3.13 python ([ee49d9f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ee49d9f7b7c98c745635c65e42625b32dcecceaf)) +* tests ([9b80096](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b80096cc975651c592adb5d1a1fe93ed5494cc5)) +* unit test ([97a3fe7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/97a3fe7a3608c4c6906fcf4d17f1016991f27595)) +* unit tests ([f1b9ed8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f1b9ed811abaac743dbad2f78b49a06397bf0036)) +* val utils ([abd0ca0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/abd0ca089e5f91ad4d458480a9a9d94b6bab7e68)) +* version ([e64001d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e64001d88a0a3dda9cc8f9e434224bc156671abc)) + + +### Features + +* added centralised logger and few fixes ([3400e4c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3400e4cc57a12f1a7f72124a3e289b51e6622a81)) +* added helper functions and fixed errors ([017e59e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/017e59eea84d5e4871d9455266ffe49c80b24036)) +* added local dev guide specific to evaluations and some fixes ([ccdce4a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ccdce4a80393340bdb8a312f65502a0f3249b04e)) +* added orch registry param for evaluation_config for simplified flow ([9a00520](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9a0052074cfdda247ed98f74a2ddc727f64e6257)) +* added results module, parsing based on run id and few other fixes ([5bd5b47](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5bd5b47ffd6a97545734b5017fb08f2dfc20dc39)) +* completed adapting the flow till dataset data extraction of provided config ([0b589bc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0b589bcdc962bef71a1eb28b76d9de58966190e2)) +* completed happy path of evaluate and the client initialisation ([2f877c6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2f877c6f9d8d694715efcfbe4548f722c8aea654)) +* corrected the single evaluation job flow and adjusted rest client to requests for metrics endpoint ([7a06324](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7a0632410ea7b422e43a779e3f913d8dc6643c56)) +* enabled the flow till testing the orch url with test config ([ee3bf98](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ee3bf986c8f933dcede1e473c77bebe7a4a9933c)) +* evaluations convenience methods sdk ([c3270c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c3270c150d5ce9b91870f8cb157ab9b02feda909)) +* extended some more functions in client and comments with placeholder comments ([0245a38](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0245a3885e375a48812cc4e9a137eed281eca5e0)) +* fixed the flow till accumulating of data across all configs and decide for single/multiple execution flow ([6c30989](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6c30989b7a8738b9b042372028710cbc12995fa7)) +* fixes for from_env function and some cleanup ([5dcfa12](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5dcfa12479e9a8cfbcba4fbbbf3ee74c4ddd0070)) +* improoved validationa and error fixes till accumulate data in evaluate ([71e5ecf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/71e5ecf5fa965a978e31ce181b205d88d651396a)) +* improved docstrings and debug_info split ([785ebb5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/785ebb5bfcc30b65e5d766a4b475e42a383c3cfc)) +* initial refactor and cleanup ([abbe8ef](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/abbe8eff397d247a20773d92ab6ce543eadf21b1)) +* multiple evaluations flow and refactor for better function split ([0bbfb61](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0bbfb61719a235648f57190ee4af49497435f459)) +* porting of entire step1 validation code from simplified executable ([72e168b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/72e168b62d5b48c88189fe3a49f1dc83183a5d9e)) +* refactoring and fixes of happy path flow till configuration creation ([53b8b7f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/53b8b7f3328af99bfd1919e40d6637f68d9d9a38)) +* updated changes and fixes with artifact happy path flow ([280c748](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/280c748fb785cd061378a5d2998c2e58b86ad01d)) +* version changes and util function changes ([c4abd44](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c4abd44f759512861c89a9d2618c1c483f5d0c29)) + +# [6.3.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.2.0...v6.3.0) (2026-02-18) + + +### Bug Fixes + +* **langchain:** upgrade langchain version ([9f57830](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9f57830aba44373cf58a06bd8808fcc8b3266e31)) + + +### Features + +* **orchestration-service:** support embeddings ([2cdd21c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2cdd21c6bedeb43cd88168977ee2582e28403a1f)) + +# [6.2.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.1.3...v6.2.0) (2026-02-02) + + +### Features + +* enable temporary_headers_addition for all API clients ([0f70483](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0f704837b7c6fabd8edbd0d038898fa853c36462)) + +## [6.1.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.1.2...v6.1.3) (2026-01-29) + + +### Bug Fixes + +* **orchestration:** use manual buffering in sse_client to handle json split across multiple chunks ([2919fa5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2919fa5f356c5920da023b5febeb0186af80938e)) +* **sonar:** refactoring ([8874bf1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8874bf197dc32bcad34eeb2baffb9c20c8bc53e3)) +* **tests:** add unit test for sse_client ([9cbd716](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9cbd7169e089dabbdf070cbce8b7aa9f78788711)) + +## [6.1.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.1.1...v6.1.2) (2026-01-28) + + +### Bug Fixes + +* Jupyter notebook merge conflict gen_ai_hub ([8f1237f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8f1237fde074d242279b402b29fb5089ec10795b)) + +## [6.1.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.1.0...v6.1.1) (2026-01-27) + + +### Bug Fixes + +* **sphinx:** downgrade version ([7cfbaae](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7cfbaae93c1c86ee5d7971076dc226320ad73f84)) + +# [6.1.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.0.1...v6.1.0) (2026-01-27) + + +### Bug Fixes + +* integration test ([d096479](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d096479e88684e0a28767d6e72f05dc53bde5028)) +* lint ([c579e5c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c579e5cda2b2866865b1f8d9a3954959a2924688)) +* lint ([71e8c0c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/71e8c0cd059b7d2432c8d62d64e0738a563c98a2)) +* lint ([f787436](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f7874361303837256062cfd00aa4636fe3266cfc)) +* pylint ([b7725f9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b7725f98e4a67540767f68acd8e558058f51d8c7)) +* review comments ([4f0eb0c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4f0eb0c80fef86e011be7cd1026558fb5948c864)) +* unit test and lint ([476eef7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/476eef708bb3c1df430e93858bc65fe4aac34129)) +* unit tests ([3b1bf85](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3b1bf859c25250e450b5d9b792513a6e05214d2f)) +* unit tests ([59ec75c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/59ec75c923cfa0ef5601a4a3eae6f1d5f98d3036)) +* unit tests ([81d4450](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/81d4450bcd9abd10de267079868d9217f540cd4c)) + + +### Features + +* add google gemini embedding model ([1c24f28](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1c24f28e28246ea47345c259b41daf7bba5c9175)) +* add google gemini embedding model ([02cb693](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/02cb6934b3651683ed457b880ddfeee7c878aedb)) +* add langchain gemini embedding ([cce1db9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cce1db9daf2644240fd1c9dc8c83d109906a515a)) +* add nvidia embedding in langchain & tests ([e9b09af](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e9b09af9103281d985274817a937b50bb740a454)) +* **embedding:** added amazon--titan-embed-image and nvidia--llama-3.2-nv-embedqa-1b embedding models ([9d4065d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9d4065dfc310901dfc261c1fdb8d163aa386d7f4)) + +## [6.0.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v6.0.0...v6.0.1) (2026-01-27) + + +### Bug Fixes + +* **deps:** update actions/setup-python action to v5 ([ead31aa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ead31aa94bdb537b1d61c676bb14bfd62b537368)) +* **deps:** update dependency langchain to ~=1.2.6 ([d57706d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d57706dfcbc2a982d35940b0eef0fa4bd303d18f)) +* **deps:** update peaceiris/actions-gh-pages action to v4 ([cb8713f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cb8713f18620df1f15b738c6442a5f0632e71e45)) +* **requirements:** update requirements ([5d7a222](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5d7a22246ee32d854cb7833b1c6495798d48c6e6)) + +# [6.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.12.0...v6.0.0) (2026-01-23) + + +### Bug Fixes + +* **pylint:** remove lint errors ([0474f2d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0474f2d2ee4fcbb8416b698618bb6395584c30cf)) +* **pylint:** remove lint errors ([562cda9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/562cda93eb4a6e25b301f843a3180569ff186258)) +* **pylint:** remove lint errors ([6ca8a12](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6ca8a1228478a5101e878ab157adcc8cc025c905)) +* **pylint:** remove lint errors ([bb5dee7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bb5dee7b8cdd6a27455ef65d3a9796ae170daac4)) +* **pylint:** remove lint errors ([2d01df6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2d01df6d4ce982140db0265e8f3c372fb2d9b7d9)) +* **pylint:** remove lint errors ([6baf38d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6baf38da64cb7f1af9f81d8e58709422ec60520e)) +* **pylint:** remove lint errors ([8cd0e0b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8cd0e0b7d8ba256e768bddcae0aa5cc46f405b94)) +* **pylint:** whitespace ([648a7f6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/648a7f6fa945699750474e3229562ca396966881)) +* **version:** dummy commit for major version dump ([9eb7c77](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9eb7c7706a135b2ebd479c87a71e00d61c13cf35)) + + +### Features + +* **api doc:** test of new api doc ([440d635](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/440d635ef139ef44a5b455fe4571f9be3580849c)) +* **api-doc:** add api reference to toc ([2940578](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2940578be9afd3006b1ee25c98b54836ca21ba33)) +* **api-doc:** add docstring ([f4f03ca](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f4f03ca22e90c05b60788691dfdc5dba434ec826)) +* **api-doc:** add orchestration docstrings ([7ddfdad](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7ddfdadabb583d0c04240ed5c2110cbff64df23b)) +* **api-doc:** add orchestration docstrings ([822b920](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/822b920ec7d4f73d92c8bcc62e28ca1c2a06d3c0)) +* **api-doc:** add orchestration docstrings ([9fad57c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9fad57c8112b35b104e5fbd2072a41bc7ad4eeb5)) +* **api-doc:** add orchestration docstrings ([cdbd7c9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cdbd7c9c4cd384cbec217376c4b8013fac3e81ad)) +* **api-doc:** add orchestration docstrings ([312b8c0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/312b8c03e36b4a4b85c26d0214d212b151a71366)) +* **api-doc:** Convert and enhance document grounding docstrings ([74afad1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/74afad163eb3634aae27e1ec2ccdf0d2725d7afd)) +* **api-doc:** refactor docstrings in orchestration ([3016f0e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3016f0eb5d6de72aea60f6c86f00b2cdd3d5b813)) +* **docstring:** add links to external docu ([00dd5dc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/00dd5dc8a041392bc8af4e8528367cb1eda87751)) +* **docstring:** add native package ([f58292e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f58292e745767e1e2ef4a112a25af7b4be912e83)) +* **docstring:** add native package ([b703977](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b703977eba384013ab6fa9ee3c466fb16024601d)) +* **docstring:** add native package ([3cba624](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3cba624502635c0f895c14b7d26f09167d328440)) +* **docstring:** add native package ([0bfe02d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0bfe02d6cbaf22387b4fd716b9f9abc70a9f3207)) +* **docstring:** add new google docstrings ([168956a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/168956a19866c4c67aba4f772e96d3f5e52dfd2f)) +* **docstring:** adjust list ([e4e0db2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e4e0db22af044945ebc46a034d0a0490a5dedf4f)) +* **docstring:** disable too-many-parameters ([8f6cc90](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8f6cc90ccf68c2ee7c4c8e3cb563ca03e122aeaf)) +* **docstring:** disable too-many-parameters ([cfdce38](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cfdce3872cb89a78834cf4fa905b99b21371257d)) +* **docstring:** disable too-many-parameters ([37654a2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/37654a2073743491ea010e15d92e97b851b0c65c)) +* **docstring:** orchestration v2 ([b5c8ab2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b5c8ab2ac03a097e8521810b7431c801042dde54)) +* **docstring:** orchestration v2 ([97459c7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/97459c71a816a94d95ec03f5fed698a49dffaeb8)) +* **docstring:** orchestration, prompt_registry and proxy core ([bbc3a5a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bbc3a5a394dd2ed46415c05b957ff5d2b63c5b02)) +* **docstring:** refactor ([1a43f5e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1a43f5e5641e6f5b85a3318d88a59efe4245bac7)) +* **docstring:** refactor ([accec41](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/accec4192cd43e5a44e0c44006312a0fbb224f4b)) +* **docstring:** refactor docstrings ([ba0ce45](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ba0ce45a8a03c8fc95e42c05d4d01e156a31e168)) +* **docstring:** refactor docstrings ([d49ff30](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d49ff304424aab9714fd1eddcec53efd0e993b58)) +* **docstring:** refactoring ([7cd1c3e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7cd1c3e42423b036146f6f1ea2b155850988a2fb)) +* **docstring:** refactoring ([e5f7a02](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e5f7a02dba0ec8fcbfcaa49f817789f0e4639278)) +* **docstring:** refactoring ([6a0d3e4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6a0d3e439c1d89e70e87c54895408fae6ff391f7)) +* **docstrings:** add docstring convention remarks ([f2c9425](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f2c94252db7b4078a20e109ab7b9b24a3819d45c)) + + +### BREAKING CHANGES + +* **version:** langchain v1 & google gen-ai sdk + +# [5.12.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.11.0...v5.12.0) (2026-01-23) + + +### Bug Fixes + +* debug unit tests ([99104f8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/99104f8cea3596ba1ccd8219822fdd0cb27b4cdb)) +* delete google vertexai sdk dependencies ([68cb6e5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/68cb6e500feacee5a7f071b3cc8894daeb18191d)) +* gemini 2.5 flash lite model not available ([1b804ff](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1b804ffc2e3b91c62dbcc8713b7090e1fc3604e5)) +* lint ([94192b6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/94192b6ddabf37d995e6cb53d28f1669fa08728c)) +* merge conflict ([39a986f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/39a986f397753cd07ca8fdbccabfbb1bc1d907e7)) +* remove jupyter notebook output ([72fcc7e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/72fcc7e26809749d99c8c997d13ff72c9a86f395)) +* remove jupyter notebook output ([3bf2e3e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3bf2e3eed8c0fe4e69e0a58b014622633b446054)) +* remove mistral test model in openai integration tests ([7d205f4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7d205f4ad4b58a7170fbcc203570bf14b93ea0c7)) +* requirements version ([24d62c4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/24d62c4fdb824db6f56c5471d082467a92fdb588)) +* revert mistral model ([16db788](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/16db78877a19c0e1112bb1b6b522a20a27f6354c)) +* review comment ([f1b2d38](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f1b2d3821cc5bf5126609104394d06aa15f09286)) +* unit tests ([953a2a5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/953a2a5c33e7acdd71005fda70ad57dd52dcd8e7)) +* unit tests ([daea4f1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/daea4f1f1cf81924411b40ebcb9cc4e1e47b20a4)) +* update requirements ([c978398](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c978398be58215025aa7979bb606ae34ed2f1943)) + + +### Features + +* added timeout parameter ([af7dae7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/af7dae7349d61632ffcbc888d2007ba8e925bbf2)) +* **google:** migrate vertexAI sdk vanilla method to google gen sdk ([0b3f628](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0b3f62865b43026668f697e2f1c47a91f5654e12)) +* **langchain:** added google genai support ([5f7a04a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5f7a04aceb5215e8efd8137800b0400a1f425ec1)) + +# [5.11.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.10.0...v5.11.0) (2026-01-22) + + +### Bug Fixes + +* **docs:** corrected aicore configure command ([8bf258b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8bf258b6579b9f82eaceee064742fc4071f9c4aa)) +* **models:** remove old models ib-granite-13b-chat, meta--llama3.1-70b-instruct and claude-3-opus ([af13224](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/af13224622fc884fdc7d780918a05299a3af1be0)) +* **test:** fix streaming test ([d63ef9f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d63ef9f9da3120a9edbf68d764e20d9be8f6e353)) +* **test:** fix vector api test ([0407aa3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0407aa352a47c2d0445ec1938b19f99ec0206de0)) +* **tests:** adjust async timeouts ([01a576c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/01a576c51b902188059546f0a55fca120459cfef)) +* **tests:** fix integration teset setup for models ([ce4d2dd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ce4d2ddc4b0c93b5ff3aa55ea53f81ffde5435a9)) +* **tests:** fix retrieval api test ([579beb2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/579beb20fc103920c25175542dad8f75c1dbdb21)) +* **tests:** fix/improve some tests ([eebe748](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/eebe748688ad76b4a0fc1fd9e139b148cbfa014e)) +* sonar and unit test fixes ([7a1107c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7a1107c3a65cf1aafcb88087c476df4175da8f6d)) + + +### Features + +* **langchain:** upgrade langchain to v1 ([7a57109](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7a571091a8648975774dee5115f02c3eae811b1f)) +* **langchain:** upgrade langchain to v1 ([569ff3e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/569ff3ec11272814c73416495571c813e80a0c2c)) +* **langchain:** upgrade langchain to v1 ([bf4b8c5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bf4b8c5ee23e8f53dc8429773af9267125445962)) +* **langchain:** upgrade langchain to v1 ([c7c706e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c7c706e778ca908b51bec899736a035d228f9cbe)) +* **langchain:** upgrade langchain to v1 ([900a250](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/900a25028fcd5b989f4013f8f61802b73a081f4d)) +* **langchain:** upgrade langchain to v1 ([2a3658d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2a3658dd13fe9cea5b7078ca72bc11afe25487fc)) +* **models:** add claude 4.5 haiku ([fcc6d21](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fcc6d21db63c9f1f6ad2def5776bf92496842ec1)) +* **models:** add cohere langchain support via openai ([b0b5bf1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b0b5bf14a384a8346c185d849af010601c75633d)) +* **models:** add cohere support for native via openai module ([2080501](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2080501890786ba067cfd4dde32b36137b0fa0df)) +* **models:** add cohere--reranker model ([2d50f98](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2d50f987af70cf8278de398de6f7375104c410ff)) +* **models:** add support for claude-4.5-sonnet, gemini-2.5-flash-lite, sonar, sonar-pro and mistral-medium ([1a0fec6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1a0fec6558eda2b6dfdf907b972733a78c3b08f7)) +* **orchestration-service:** add support for v2 api ([b0ff77e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b0ff77e45a09dda76d69b65ee4c313b9ad142924)) + +# [5.10.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.9.3...v5.10.0) (2025-12-09) + + +### Features + +* **orchestration-service:** retries with exponential backoff-AIWDF2703 ([f720576](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f72057679a9921e2c4085c9e04cd4449694164fb)) +* Initial Commit: Implement Retry logic with exp backoff [AIWDF-2703] ([3c59541](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3c595419ec05bc4e79a0113c02393a41b1eb185b)) + +## [5.9.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.9.2...v5.9.3) (2025-12-08) + + +### Bug Fixes + +* **deps:** update dependency pytest-cov to v7 ([7b92331](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7b923312c0a1057fca847c63cf9b97baa9673264)) + +## [5.9.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.9.1...v5.9.2) (2025-12-03) + + +### Bug Fixes + +* **tests:** use gpt-5-nano-latest config instead of retired gpt-4-latest ([fa93ab4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fa93ab40e91da80e67fc0e0fa85035743906578a)) + +## [5.9.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.9.0...v5.9.1) (2025-11-29) + + +### Bug Fixes + +* **deps:** update actions/checkout action to v6 ([8d56565](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8d56565776d486c4f48833cd6838bcd7641da719)) + +# [5.9.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.8.4...v5.9.0) (2025-11-27) + + +### Bug Fixes + +* **test:** reload classed so that env vars are picked up ([0563e49](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0563e49d2666317d739b3d97590126e133d3b142)) +* **tests:** minor test tweaks ([689e191](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/689e1915410c204d9480adaf83b3178619acc6af)) + + +### Features + +* **client:** add configurable ai_client_type header ([7cc509b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7cc509bd622892baa0e55f79e9d536d1a9653227)) + +## [5.8.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.8.3...v5.8.4) (2025-11-17) + + +### Bug Fixes + +* **auth:** skip token generation when env is set ([8226fd6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8226fd664db8c00886fea756fb1c920f7c8edc2c)) + +## [5.8.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.8.2...v5.8.3) (2025-11-07) + + +### Bug Fixes + +* **requirements:** update pydantic ([686ce48](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/686ce488a2d7f4b171eb435c89b4b023944b6b89)) + +## [5.8.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.8.1...v5.8.2) (2025-11-05) + + +### Bug Fixes + +* **requirements:** update aws dependencies ([d1a99e4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d1a99e47277c0ffa93113b47dd7661e311993bad)) + +## [5.8.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.8.0...v5.8.1) (2025-11-04) + + +### Bug Fixes + +* **orchestration-service:** document metadata filter as list according to orch-service api ([2c941df](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2c941df52628da51ef879bba9362e9e1d0f1a230)) + +# [5.8.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.6...v5.8.0) (2025-10-30) + + +### Bug Fixes + +* **document grounding:** fixed tests ([8589c9a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8589c9a067d5d817c2b336b05bc3585ec1eb0a78)) +* **document grounding:** fixed wrong destination in test_executions ([f291024](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f291024102abbded459021037e9c8c90495f9420)) +* **document grounding:** fixed wrong param in pipeline trigger ([33dbdb6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/33dbdb69b49d7121a86f09bd956aa2cf9005f542)) +* **document grounding:** fixes after code review ([c2b8fcc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c2b8fcc8529cd8432dea736dadddf9bf2e0c6b8d)) +* **document grounding:** fixes after code review. Added imports of new clients to client.py ([6c1f710](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6c1f71093cb48cf2d79b656ff68bdf7102f2b697)) +* **document grounding:** test commented ([2d0f431](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2d0f431581a061ec3e516824229d14e5893c5230)) + + +### Features + +* **document grounding:** add vector api, retrieval api support. add missed methods for pipelines api ([7d01f99](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7d01f99196ccbc60d7e7e1ddad7982d4aa515f30)) + +## [5.7.6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.5...v5.7.6) (2025-10-13) + + +### Bug Fixes + +* **orchestration-service:** bring back timeout for requests ([c8f5694](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c8f56949ccedb1625141008d21396e1b594632f1)) + +## [5.7.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.4...v5.7.5) (2025-10-01) + + +### Bug Fixes + +* **documentation:** provide documentation for 5.7.5 ([a75d93a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a75d93a9de6e5efe3c4914a73729c14ebd12c9ca)) + +## [5.7.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.3...v5.7.4) (2025-09-29) + + +### Bug Fixes + +* **tests:** bedrock invoke does not work with amazon-nova models ([3729c91](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3729c911d110eeaef5555698aa24f262d3a53921)) + +## [5.7.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.2...v5.7.3) (2025-09-29) + + +### Bug Fixes + +* **boto:** fix unit tests ([4ad86a5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4ad86a5c02345532bf9d0d0b514c19bc0e761782)) +* **langchain:** Keep boto config in session ([794e741](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/794e74189b0f3066217fd8080c4f0d934f542bf7)) + +## [5.7.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.1...v5.7.2) (2025-09-24) + + +### Bug Fixes + +* **model:** Allow boto config for Amazon Bedrock ([eb231c2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/eb231c2e865abc84ed22c9c370bedf1a16179b6e)) + +## [5.7.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.7.0...v5.7.1) (2025-09-24) + + +### Bug Fixes + +* **langchain:** used a fixed version of boto3 ([148aeca](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/148aeca3f2d67686273bb5e30ebcdbe902f837b9)) +* **openai:** o4-mini and ibm granite unusable due to comma error ([d3f5c77](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d3f5c77da99c49fba7cadfde58d51b7bc37c9029)) +* upgrade langchain_aws library ([c09c913](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c09c913263a5d3be1a5048439991171b7b4a7d0c)) + +# [5.7.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.6.4...v5.7.0) (2025-09-19) + + +### Features + +* **models:** add gpt-5 and gemini-2.5 model families ([9ec7f54](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9ec7f54e0fdc27dbd48eaaa0250c2e4de053d9b4)) +* **models:** add gpt-5 and gemini-2.5 model families ([36d4a3c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/36d4a3cbc793a7bbee632c57170a12e81586c61c)) + +## [5.6.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.6.3...v5.6.4) (2025-09-18) + + +### Bug Fixes + +* **model:** missing removals ([e596dc3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e596dc345751f1d780d203c2f19f880c1df16390)) +* **model:** remove anthropic--claude-3-sonnet (no longer available) ([31acc2e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/31acc2e7d8f1f0af34600f4f74d1f65c5e57b996)) +* **model:** remove claude-3 ([7c07c0c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7c07c0c48b776cf7128b197cef799f34117bdba9)) +* **requirements:** update vertexai ([d994a10](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d994a10190712010199cfdd77de8b4bcd21a12c3)) + +## [5.6.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.6.2...v5.6.3) (2025-09-10) + + +### Bug Fixes + +* **orchestration-service:** return headers in OrchestrationError ([c76a6ce](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c76a6ce433cf0ed2a48df12caa7a18892423a9ef)) + +## [5.6.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.6.1...v5.6.2) (2025-09-08) + + +### Bug Fixes + +* **orchestration-service:** show deployment not found error ([b640100](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b640100f7f3c5aae74446ff75fa33e36ced85b76)) + +## [5.6.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.6.0...v5.6.1) (2025-09-08) + + +### Bug Fixes + +* **orchestration-service:** return token usage in stream response ([a7bdb0d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a7bdb0dd4a85d64e5788be64649d448506f597e2)) + +# [5.6.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.5.0...v5.6.0) (2025-09-03) + + +### Bug Fixes + +* **models:** handle gpt-5 like a reasoning models ([f994ce4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f994ce459adff0849f1f680b1ac693623008fd78)) +* **pylint:** fix annotation ([49aa506](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/49aa5067a18bb73519706dd7f619ca6584bd55e3)) +* **test:** fix async test ([03c9cd3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/03c9cd3500549f71d14f39b233fe38bf7d6d2bd1)) +* **tests:** fix unit tests ([d55bf56](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d55bf56393cb7be520674b8b9fe74d3d9ab7766d)) + + +### Features + +* **amazon:** add support for event stream ([9d20a82](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9d20a82e62014a3f26ef32d1f3cee5817508c194)) +* **models:** issue deprecation warning if timeout parameter is used for bedrock ([3350b68](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3350b689bd33beaac3bd77a8f10d95632dc09d0e)) +* **models:** use proto parser instead of selfmade solution ([79cc241](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/79cc241ffe75d7a080a7e3b9e5a5960f385b6a5d)) + +# [5.5.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.6...v5.5.0) (2025-08-27) + + +### Bug Fixes + +* **AIWDF-2642:** fix camel case bug ([#477](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues/477)) ([70ae1be](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/70ae1be8b0de3a99b22b2d658458cb518f4dddf0)) +* **blackduck:** adjust blackduck dockerfile ([6404890](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/64048900b9f661a0304651e1be70b7c29c36db36)) +* **blackduck:** check blackduck run ([36eebf5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/36eebf50754ef158d7d48108d607ab1ff4311ec2)) +* **blackduck:** trigger main build ([ace180e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ace180eb03eb4743bc2cd17e0b66688da118e124)) +* appease qualitygate gods ([2ccb616](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2ccb6164cf3369dcb0641640186b707b06645325)) +* line length ([21365d4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/21365d4a0414943a33cd66eef0e4215cc75e635a)) +* read response for streaming errors before handing the error to callers ([55ce903](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/55ce903667ca33e2a253a017801fdb2ac26ae9cb)) +* remove root_async_client from kwargs in Completions and AsyncCompletions classes to streamline model creation ([9b269b8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b269b84f8b371f1d9cab33a0812c25b7712ee7f)) +* remove root_client from kwargs in Completions class to streamline model creation ([ad33b8e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ad33b8e572becadb1ae7dbca101a52278612d5ab)) +* unused typing imports ([8ffa0c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8ffa0c1b7cfb4a30a6286e4f960f9b9654ec2252)) +* vertex client can now process multiple chunks at once ([da7392d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/da7392de2987d9a9dcdbf0ddb94d791532f17311)) + + +### Features + +* **model:** added claude 4 sonnet ([4df0c98](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4df0c9809eba1fcf95c12bd3c0e11a69d392d583)) +* **model:** added commented-out gpt-5 ([a32a611](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a32a611ee489b18c42f728ec54e44eb259cc202f)) +* **model-output:** added correct commit message ([d775718](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d7757181f81abe7adbb593b8d93d4ddbc07b044a)) + +## [5.4.6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.5...v5.4.6) (2025-08-06) + + +### Bug Fixes + +* **docs:** Remove redundant api_url argument ([#468](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues/468)) ([243d5ea](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/243d5ea484e62ec6bad0e0969217902d64f9c424)) +* **requirements:** update google-langchain ([96da7a8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/96da7a8e391fcc4f250a663460dd739d9e5e87a4)) +* **setup:** switch to python 3.13 ([96f4c32](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/96f4c326f200dc1433519d17ed6d982b58dd2c00)) +* **test:** review comments ([e5058cc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e5058ccad9dadbae3814370630ae561f1395ddad)) +* **test:** review comments ([a6fdb7e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a6fdb7e882ce42b0f27e17fc9f9291ea56d5dd9b)) +* **test:** review comments ([e317417](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e3174179eacdd5c45991fea10b9845183586c848)) +* **test:** review comments ([dd221a3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dd221a3165f008b71e546b8f798c084c8fa7d994)) +* **vertex:** fix issue in client initialization ([0946ee2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0946ee2c7d118733ac20996e14a6099c4d509c20)) + +## [5.4.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.4...v5.4.5) (2025-07-30) + + +### Bug Fixes + +* native amazon configurable config ([8ea9ba9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8ea9ba9e974c39ed072ed19c4fae3f97233a9729)) +* test ([46febbe](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/46febbebaec017a4a474f2877c2099ba6eba55a1)) +* typo ([2e7225f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2e7225f5a8d2d09d8d99c16cf8f1fdedab80e2b0)) + +## [5.4.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.3...v5.4.4) (2025-07-29) + + +### Bug Fixes + +* **doc:** Provide CAM profile details for contributing to sdk ([5d76e66](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5d76e66dcf8ffdd5ffd3af3ad4b307a6156f8e18)) +* **doc:** Provide link to commit conventions ([0fd48dd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0fd48dd76c2fedec1897afaaed5c91677e3d5f1b)) +* **test:** orchestration service only supports tenant scoped prompt templates ([981eb7a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/981eb7a70942a7bbc18b1e3ecb1c54c635a801b8)) +* **test:** orchestration service only supports tenant scoped prompt templates ([7bb88bd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7bb88bd1e9386f3b3114f9032fc6158d49d6e8df)) +* **tests:** revert rg scope ([e7111c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e7111c124c5cd33d60829bffb155f324b8c950eb)) +* fix translation component test ([e201d02](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e201d021d0ff4a08d193439c347963b0e40aa841)) + +## [5.4.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.2...v5.4.3) (2025-07-22) + + +### Bug Fixes + +* **models:** o4 model not available ([c13aed5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c13aed5078660d2298136a6c34ed061b943586eb)) + +## [5.4.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.1...v5.4.2) (2025-07-03) + + +### Bug Fixes + +* **requirements:** update ([2359ebb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2359ebb6d254fccb9b2f680cf75398efcaf159dd)) +* **requirements:** update aicore dependency ([e1bd926](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e1bd9267efc1ede3c5ecaad1c3fe4b71a56fde6a)) + +## [5.4.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.4.0...v5.4.1) (2025-06-26) + + +### Bug Fixes + +* **install:** add models for which langchain is installed to langchain init ([819de6d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/819de6da7e4a51ceb87bc8a202e502efce1dd1e7)) + +# [5.4.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.5...v5.4.0) (2025-06-26) + + +### Bug Fixes + +* **requirements:** Update requirements.txt ([9f3e52a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9f3e52aba3ea17b3fd88263c42e90aad38165a82)) + + +### Features + +* **orchestration:** add image support ([061b6f1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/061b6f11dccf3a7e3702c7f34d4b4d9c8ee7f321)) + +## [5.3.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.4...v5.3.5) (2025-06-24) + + +### Bug Fixes + +* **streaming:** add timeout parameter ([8c89786](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8c897868ce599faf72281efa5a54ba30846bc7ad)) + +## [5.3.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.3...v5.3.4) (2025-06-23) + + +### Bug Fixes + +* **docs:** documentation adjusted ([e76ea16](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e76ea16f427466fea55f8383412fa9299da799c0)) + +## [5.3.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.2...v5.3.3) (2025-06-23) + + +### Bug Fixes + +* **metadata:** additional obsoletes-dist fix ([c781942](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c781942366f842f8989519ff60eb06ab305df689)) + +## [5.3.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.1...v5.3.2) (2025-06-23) + + +### Bug Fixes + +* **metadata:** correct obsoletes-dist entry ([d2d59fb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d2d59fbd49e9ba62ca4eed73ec199cf83a8be25f)) + +## [5.3.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.3.0...v5.3.1) (2025-06-12) + + +### Bug Fixes + +* **docs:** documentation for 2505b release ([31643db](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/31643dbda39a199353e613816cc81bc96708eb79)) +* **tests:** skip flaky test ([29e5178](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/29e5178d7d9bb5c2ff5e591882c1b1a6f929858a)) + +# [5.3.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.2.0...v5.3.0) (2025-06-06) + + +### Features + +* **AIWDF-2536:** add translation module to orchestration service ([#427](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues/427)) ([0971f44](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0971f4493760102d6570b295fb97171f756fadec)) + +# [5.2.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.1.0...v5.2.0) (2025-06-06) + + +### Bug Fixes + +* **chore:** adhere all python files to PEP8 style guide for imports ([a25e5f1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a25e5f1c7c1f9c95b2af5cbe508f66d6261f1f24)) +* **chore:** fix imports after merge ([9b5feaa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b5feaab913c333d5d53f22c87f87e44699de0c3)) +* **docu:** fix formatting of langchain matrix in docs ([5f258bd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5f258bd38203298d652a7c0858a65b6e0190b9cf)) +* **requirements:** update core sdk ([ebcf12f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ebcf12f027dc6c20f60a044f9575e1e64b8aef35)) +* **xmake:** update config.yml ([1a9f33d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1a9f33d70a57e810395c55f283509798c67b693e)) + + +### Features + +* **tests:** improve streaming tests to ensure that chunks are not buffered and returned at once ([2d81257](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2d81257b9ed0bc179ffd74e911d468129c1c3c5b)) + +# [5.1.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v5.0.0...v5.1.0) (2025-05-30) + + +### Bug Fixes + +* **requirements:** fix httpx dependency h11 version ([ea567c8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ea567c8461752b46223958ab74d196d8a1262dbd)) +* **requirements:** loosen pydantic requirement ([ce36ef4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ce36ef4d90bb06aab48b82f77decefebaf452ad9)) +* **test:** unskip tests ([53b5459](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/53b5459aa43be9770afe9cc530e3cebf6dfdc847)) +* **tests:** temporary skip mistral-large model ([4505b15](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4505b15b377f82a2c98de68a5cf234fd119bd070)) +* **tests:** use self.assert instead of assert ([00d8f51](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/00d8f51a915ee4b2614dc9d4761d90b20062bc8e)) + + +### Features + +* **docs:** improve documentation for install parameters ([94b3672](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/94b36727cccb0ab26aca84354597a5467945b169)) +* **langchain:** remove langchain from installation parameters ([0d8e12c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0d8e12c0808a4171b133e994c51e7c62f6b55d63)) +* **langchain:** remove use of vendor specific classes in __init__, adjust setup to only install vendor specific langchain libs on demand ([1a6869e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1a6869e2fdf229bac2786095ca2b649db1aa3231)) +* **orchestration:** add function calling ([d9f82c3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d9f82c3d2971be5c3876265f48d5b1a6219f1b71)) + +# [5.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.12.4...v5.0.0) (2025-05-15) + + +### Bug Fixes + +* **tests:** temporary skip mistral-large model ([c78654c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c78654ca1ad0311f9685da710ef9ee517a223148)) +* **tests:** temporary skip mistral-large model ([ff8aeef](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ff8aeef38bc0de33c8bd342b5883f7c1445c758e)) + + +### Features + +* **models:** support new openai models ([54abe80](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/54abe8095c9a388b1e772b2a42e67ba8a4b21302)) +* **package-name:** rebranding-AIWDF2526 ([c04d812](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c04d812904a4d34ee85f94861360cb055eca2c1a)) +* **package-name:** rebranding-AIWDF2526 ([4c8832e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4c8832ecbd561d5f2535bb4baf08c8282008cf75)) + + +### BREAKING CHANGES + +* **package-name:** new package name sap-ai-skk-gen + +## [4.12.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.12.3...v4.12.4) (2025-05-14) + + +### Bug Fixes + +* **revert:** revert translation module ([fb960e0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fb960e0eae88ae45962e0f86e519442112c9dcca)) + +## [4.12.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.12.2...v4.12.3) (2025-05-12) + + +### Bug Fixes + +* **native-client:** use stream instead of post for async streaming ([61fdf27](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/61fdf27ca9ecd6e6e632037b0215033f40f608c3)) +* **test:** fix and reenable unit test ([d7d5a33](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d7d5a333fd37885a4c5f90a6d6fcfe5e807425a6)) +* **test:** test for number of chunks > 1 for async invoke stream, adjust stream generator ([87554c2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/87554c275cab5a485af24b6dac1d34eb38ed7bff)) + +## [4.12.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.12.1...v4.12.2) (2025-05-05) + + +### Bug Fixes + +* **blackduck:** Updated the ctp detect token ([e4b1743](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e4b1743560c1c9de73233b6b99490bf7372ddeda)) + +## [4.12.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.12.0...v4.12.1) (2025-04-29) + + +### Bug Fixes + +* **requirements:** update core-sdk ([e44a294](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e44a294e2a4e07bc2506122561757f6d10d312d3)) + +# [4.12.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.11.0...v4.12.0) (2025-04-29) + + +### Features + +* **deprecation:** rebranding-AIWDF2525 ([012abfb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/012abfb1f8d80f671207a3c2c9d7bd90600865e5)) + +# [4.11.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.10.2...v4.11.0) (2025-04-28) + + +### Bug Fixes + +* **examples:** add examples for bedrock and vertexai async support ([2ea2f6d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2ea2f6d373797a061f7b07760207f9aed774bd38)) +* **examples:** fix indentation ([c2e6ec0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c2e6ec01fbf342962420f9efc081a227270fd393)) +* **examples:** remove unused import ([f0f01b2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f0f01b277ece8ad6fa6290993e6bb72dd1076c91)) +* **examples:** update doc source ([2c394ee](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2c394eedae83ef737f2746561e139139a1880ebb)) +* **genai-models:** Add gemini-2.0 model family to README_sphynx ([1b83d69](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1b83d6916da75bffd5e9f1fd550df30df7c24638)) +* **test:** adjust exception message ([bde2317](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bde2317a00a688c468ec6f8a4b0906589ac3f17e)) +* **test:** comment out broken parameters (rate limit issue) ([eeb82fa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/eeb82facce816abfe6ec2002f9286bd02831b93e)) +* **test:** fix expected exception type in unit test ([5e6a8a3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5e6a8a39d4b025ac3d429e0e0984cb77447dfcad)) +* **tests:** add missing cleanup to template import, add dryrun parameter to cleanup script ([17ebe3d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/17ebe3dff48fbaa667807fd4b0d9263bbaf74962)) +* **tests:** revert back ([e15f9fb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e15f9fb5ad80a75ef64c94914e7d37a0b04cd2c5)) +* **tests:** skip flaky tests ([93b2731](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/93b2731862f3a1660ea6ea09d395e3b1d04145a7)) +* **tests:** unskip tests ([c7ddca1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c7ddca143b537a493ab7e847dda0facb64dfbecb)) +* **tests:** use correct RG ([1c62854](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1c62854f4ed1a101e6d18db7c5afd9023354a121)) +* explicitly init Langchain's converse abstraction ([57449ad](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/57449ad2bafa830ca69507a4e151c925b9f370c6)) +* remove unused async client for Bedrock ([883db48](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/883db48c6042d1abfabd524130e19551319a43fb)) + + +### Features + +* **genai-models:** add support for gemini-2.0 and gemini-2.0-flash ([fe90db9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fe90db914a4971850389de689243eb46ffbdee1a)) +* **model:** Replace mock data ([76931cb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/76931cbb8a0b30365041748fd1844080ba4809b6)) +* **model:** Retirement of Gemini 1.0 Pro - AIWDF-2524 ([17327f3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/17327f3bd84acb3bbca586ac7583ca47939eed56)) +* **model-support:** add Anthropic Claude 3.7 Sonnet ([2c1d49b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2c1d49b0cbbf9633148467ac162c7f4f758413bd)) + + +### Performance Improvements + +* **orchestration-service:** improving performance with reusable httpx client - AIWDF2489 ([fbb5c43](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fbb5c43f734fabc3125b450ebeb3df8e5050084d)) + +## [4.10.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.10.1...v4.10.2) (2025-03-26) + + +### Bug Fixes + +* **orchestration-service:** inconsistencies due to deprecated content filter parameters - AIWDF2481 ([82e48a2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/82e48a299541a7ded9145a25f56a81e3c2b32bc2)) + +## [4.10.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.10.0...v4.10.1) (2025-03-24) + + +### Bug Fixes + +* **client:** add type ([131dd16](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/131dd16ab4b86559e64e65ba3d4e201c1f1d7661)) +* **sonar:** fix sonar issues ([c03b39b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c03b39b214bbcf4c1035afc5bf087289e2a61c5d)) +* **sonar:** fix sonar issues ([f69930c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f69930c9b2bca79c083a29c91eb76682d370e58c)) +* **tests:** add langchain astream test ([8711f7a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8711f7a1d4cd0c6117c457a2a744795776c2fffa)) +* **tests:** add stream async test ([cac1ef3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cac1ef3b87227fd9e86b3c25c68e6a1c69672dbe)) +* **tests:** add unit test for async iterator ([b600283](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b600283059f25e66873214a5137a8515c85e8990)) +* **tests:** move mock class for common usage ([6580c55](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6580c5544a5d9ab7e9268fb18879dcfa65dd5680)) +* **tests:** remove dict ([1ffac2f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1ffac2f460d634f13fa6196b2b30353072db78d1)) +* **tests:** rename for better readability ([4a8912b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4a8912bbd54949011ede089b2bdc102a4e163c32)) +* **vertexai:** fix typo ([bcacbc6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bcacbc6cf748d78ec59ca8265747f07489d5cd81)) +* **vertexai:** fix typo ([27b9cb3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/27b9cb3cbf957ba8bdd8f3c6686c4302d35a9434)) +* **vertexai:** support async streaming ([77fda9d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/77fda9d07561dd26891e172250aa9631ee6208bb)) + +# [4.10.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.9.0...v4.10.0) (2025-03-18) + + +### Bug Fixes + +* **async-gemini:** add credentials ([90bfcd3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/90bfcd38049304b7515149f0c72bb16f567777dd)) +* **async-gemini:** support async ([bed0828](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bed0828dd3e1cf18fb7f565d1e5e8e11c746c5a3)) +* **client:** fix sonar error ([7fd0cdf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7fd0cdf7ad8bfd6454339606a0305e91accc2b2c)) +* **client:** fix sonar error ([54dc868](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/54dc8685dccb1f921ca2d97ee91673e3378d8186)) +* **credentials:** remove overridden class ([f389e4b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f389e4b820990f7eadce4a79eadc2db4bbcd9766)) +* **doc:** cleanup ([222006a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/222006af4788ea27e3f6a739b175721986310986)) +* **gemini:** cleanup ([c93930c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c93930c571e46cebc81bdc9183b3c1e4e1bcf17c)) +* **langchain:** add async client test ([c7f2308](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c7f230890604d4d7b3ba100c8a8eca3e88b380c3)) +* **langchain:** use ga endpoint version ([c2adf69](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c2adf69c18dde2d8725584bb1501024bb663497c)) +* **metadata:** extract metadata ([e290cd5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e290cd58d29d6ff4583c2782c467485a6cd1cdce)) +* **tests:** add test ([a7123cc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a7123cc996c07c1f003948576ef260f8a5d5dafb)) +* **tests:** cleanup ([b366215](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b366215cfccae4dc841216b6681acef415fbe874)) +* **tests:** test one model for additional tests ([a98c76b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a98c76bb959a29f7165faa4b94b24ee2be74f0fa)) +* **transport:** fix transport assignment ([598d032](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/598d032aaea4e885cbd21e3f18a7f9cc83b636af)) +* **transport:** remove patched class ([a07e8f8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a07e8f850d26e11853b9eb79304cafbc3aacce1d)) +* **transport:** remove patched rest transport ([9ebfdf9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9ebfdf9130758f85e430a0651a0506c11e153455)) +* **vertexai:** use ga version ([fb696fa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fb696fa525127a15c3ca1fd839491bbcc9070ada)) + + +### Features + +* **langchain:** add async client ([238c94a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/238c94a7e7a5aa0f5aad15ffd263dc54011794f4)) +* **langchain:** use genaihub client for langchain ([ef07e2e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ef07e2e5e4322114f821732f4e3e9995866c552a)) +* **test:** test new model support AIWDF-2183 ([#381](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues/381)) ([29d2d69](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/29d2d696b27020b69a7ddc7851f7b1f5960d599a)) + +# [4.9.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.8.0...v4.9.0) (2025-03-17) + + +### Features + +* **orchestration:** add allowlist and mask_grounding_input in data masking ([58f09cb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/58f09cbf8b4e1bdf285028a9ef90418bf19665c4)) + +# [4.8.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.7.0...v4.8.0) (2025-03-12) + + +### Features + +* **tests:** enhance documentation ([5aca7f4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5aca7f4c6eb7825e3ab20c48ca3aabed51cf6598)) + +# [4.7.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.6.0...v4.7.0) (2025-03-11) + + +### Features + +* **genai-models:** openai o1 o3-mini models-AIWDF2303 ([4bb9917](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4bb99170adc52b27482ca76f8437106ecdb9af6b)) + +# [4.6.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.5.0...v4.6.0) (2025-03-10) + + +### Bug Fixes + +* **doc:** fix typo ([0616adb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0616adb500195dea14fb848f48a65b3600a6d80c)) +* **doc:** update doc for titan model support ([8f806f7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8f806f762893e954c6e131541e4fe14e3f1eb4b4)) +* **doc:** update docs ([36d6ac4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/36d6ac48ee937c458ab434df426d52791cd8d00a)) +* **jenkins:** use master branch ([3027701](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3027701d65444a6b9a461867ae2f5034cd9f46c5)) +* **test:** add unit test ([1364eee](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1364eee09c04fdfb5e3669f62cd8456abf1ba018)) +* **test:** add unit test ([5f4a1c2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5f4a1c201235d289b24742da963ce3ddc748cef1)) +* **tests:** converse test for titan models ([56774a3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/56774a363aaa925671770f206db1279fe387bbaa)) +* **tests:** fix typo ([27710aa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/27710aab67d3537ec293acd322d1869b26524d60)) +* **tests:** fix typo ([de50bfb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/de50bfb0fd213fa64b173cc7d3944b0a4c77d2a0)) +* **tests:** update libraries ([2037793](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/20377939935dd7986debb3509535d7a1a89c4591)) +* **tests:** use converse for nova models ([44f3001](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/44f3001671829abb2ee3789dc9841ef424d071f2)) + + +### Features + +* **model-support:** support amazon nova models ([bf15eca](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bf15ecafd9d9f333cebad776d5169fd9ffe779f6)) +* **test:** add test env ([37e5069](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/37e5069cf4dd555768117f8d092a020ea60dc6ba)) +* **test:** change dockerfile ([2a75eb3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2a75eb335c37fe5050b6a41a1f21acee1f314637)) +* **test:** Enable testing in add environment - AIWDF-2419 ([95f06b8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/95f06b865d70f2a28181c09f26bf214e69849d4c)) +* **test:** refactoring ([69deff7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/69deff74da443543a1ebfb8577a5d9ba8362586f)) +* **tests:** add bedrock mark to new test classes ([d1dad4a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d1dad4a9dae4186d423bcff9895c778ca646969f)) +* **tests:** add bedrock marker to default acc tests ([41650cf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/41650cfeea4ad5c5688efb6e77073f1b7bcf9359)) +* **tests:** add pathprefix to archiveList ([98bfd66](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/98bfd668732173b08a7e95fd2efc75521bb3a9cb)) +* **tests:** change archiveList ([35dbe9b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/35dbe9be55fcf74f7671b682215509e156fc05f5)) +* **tests:** change archiveList pattern ([02f76d9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/02f76d91826a9e28aea62d23f8c759fa21e8d703)) +* **tests:** change file path for results ([0c1efa9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0c1efa923ad635fd9bde6cf40904411ebda290d9)) +* **tests:** enable default acceptance tests ([409b1bf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/409b1bff94d1792dbca9798be561aa699a14a535)) +* **tests:** enable markers for bedrock models ([b1a8efe](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b1a8efe1817db29195dead98b4783b0568157eff)) +* **tests:** new file for report us10 ([90bebbc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/90bebbc323a24456c09b7bb7a54d2bf5c1eb5fa1)) +* **tests:** remove marker from default acc tests ([9a66241](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9a6624131d97fad8cb661017fa9bd444f7c8dee4)) +* **tests:** rename the custom test stage ([110e12d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/110e12d04e4cbccdb63b5d0e67438fe1bcfd8bd2)) + +# [4.5.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.5...v4.5.0) (2025-03-05) + + +### Bug Fixes + +* **amazon:** add async client for langchain ([594916b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/594916bab338f130252aae5fdab644443e1aae3f)) +* **async:** fix tests ([db9108a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/db9108ae73cd491bc4ce55d0669050c0187b3dbb)) +* **async:** refactor ([c73c5f7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c73c5f70b26caa0b5d44116908f09c3e1bf96995)) +* **async:** use aiobotocore ([d21fd91](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d21fd91e8b51435b9f92de1c259509eee539c9d1)) +* **client:** cleanup ([f561d4e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f561d4e74ce10437fc765507236ac2638f6e64f1)) +* **client:** fix pylint errors ([5e4d5eb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5e4d5ebf3dbbffffdda230948730a7a9c6e946ca)) +* **client:** fix pylint errors ([47ac23e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/47ac23e264cf571abb8794b7738fb5f07292eb0e)) +* **client:** rename client ([02eecd7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/02eecd727fc02ad2279b82fb14dfd0f368d1539a)) +* **pylint:** fix generic pylint errors ([f11ded0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f11ded0439e205646450c84c4a958e06706d8d01)) +* **pylint:** revert changes here ([2ce4b60](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2ce4b606cc3598da484b2af1385d711c532aba3c)) +* **requirements:** add missing req ([65031b9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/65031b96e220267d78ca3dc92942da8f4edbf5e4)) +* **test:** add test for streaming ([a7c5598](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a7c5598a36e32b269d8cf18f9937e7e06e9d2206)) +* **tests:** introduce must_pass for only single model variant ([32ca5c6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/32ca5c62ae12c4c53db66c91ebce2c17ca5595ea)) +* **tests:** use constants ([e93ad03](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e93ad037da91f114b00e38f8ce48c4f93bebc015)) + + +### Features + +* **async-support:** native client support for amazon and anthropic ([10ea2c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/10ea2c170fb3e692d7bd2f6e6e070e81d44b69e9)) + +## [4.4.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.4...v4.4.5) (2025-02-27) + + +### Bug Fixes + +* **deps:** update actions/checkout action to v4 ([740296f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/740296f946bb94cfcf7dfa693009d9cce67b1c7e)) + +## [4.4.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.3...v4.4.4) (2025-02-27) + + +### Bug Fixes + +* **openai:** add back llama3.1 ([ae912fb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ae912fb171dc4dfb47016d613392fed3f69c1d16)) +* **pylint:** fix pylint errors ([10d7671](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/10d76710066d5b82735b761f61bc7eac1a3f4723)) +* **tests:** add default model back ([3ad765b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3ad765be0e95a71af124371b5d47972c7cffea2c)) +* **tests:** fix typo ([878a15a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/878a15a35500f2768a4783a0b11b0198f593dd76)) +* **tests:** remove unused import ([58bbbd3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/58bbbd334e1898f148699b9b2f2f3542fdb0383e)) +* **tests:** run tests ([f84d468](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f84d4689b9ac14823b414dfe1a68838fed9d20d1)) +* **tests:** skip flaky tests ([0be726e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0be726e527ba901c7d43ebebdce7fc28a61d68c7)) +* **tests:** skip flaky tests ([ed32cc3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ed32cc3439efb210ca8e055c17e58613209e3870)) +* **tests:** skip flaky tests ([4982762](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/498276278d35e8d3aba48014eb166c1d3ac4bd2d)) +* **tests:** skip tests ([290e674](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/290e674d2636d5c0349c8e11bfa193a12f03ec50)) +* **tests:** skip tests ([7fbf30f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7fbf30f89ac8d853f43e5c9deab9045ad7250408)) +* **tests:** un-skip data masking tests ([19f55bc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/19f55bcaded867f3c572a6004d93a21d8895d632)) +* **tests:** un-skip llama model ([8d5bdc6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8d5bdc6dff63bd313d4b9380e5382b5ef04d5b11)) + +## [4.4.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.2...v4.4.3) (2025-02-18) + + +### Bug Fixes + +* **grounding-response:** support metadata keys in grounding result - AIWDF2368 ([a293abe](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a293abe94d2107eef5f9832d0c33546f70e885a3)) + +## [4.4.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.1...v4.4.2) (2025-02-17) + + +### Bug Fixes + +* **orchestration:** improve streaming interator and timeout type hints ([#361](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/issues/361)) ([c4c054a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c4c054a2b6cca3959ecac0bd20f1ed4dea200d8f)) + +## [4.4.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.4.0...v4.4.1) (2025-02-17) + + +### Bug Fixes + +* **grounding-response:** support metadata keys in grounding result. ([d20509b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d20509b39e4cbc2a8b0c2fea90643bb243a66b91)) + +# [4.4.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.3.3...v4.4.0) (2025-02-17) + + +### Bug Fixes + +* **blackduck:** blackduck ctp scan config ([3206c4e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3206c4efad45c3d705ee4e87575301f62f60ab62)) +* **grounding:** retrieve grounding module result ([4d1fc7c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4d1fc7c6d6bfee23874691d57f4b4ac78a125f23)) + + +### Features + +* **filtering:** add documentation ([2ed24ca](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2ed24ca1abbae819ee44f03941df4a145321f49c)) +* **filtering:** add integration test ([3e01ffd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3e01ffd3bb9946983a3446a6f872a4ead51cc46d)) +* **filtering:** Add llama guard 3 - AIWDF-2384 ([d783916](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d783916ab260bc1f2d666113357df44769e41736)) +* **filtering:** Add llama guard 3 - AIWDF-2384 ([307a608](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/307a608369faede52ef1c4716725f7731d52fe41)) +* **filtering:** Add unit test ([7fbb931](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7fbb93185cafe603a52511298b6c5b127c2a6056)) +* **filtering:** Remove code smells ([b131f6f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b131f6f4e5e142d429c70c65f046981663d7666f)) +* **filtering:** Remove code smells ([926e340](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/926e34002f6b449570845b533eadc2f976ebbdc4)) + +## [4.3.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.3.2...v4.3.3) (2025-02-12) + + +### Bug Fixes + +* **orchestration-service:** fix error handling in the async streaming case ([fff52f5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fff52f57d0ea4f62dd7241eaf214b11d818255f9)) +* **orchestration-service:** integration tests ([777496f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/777496f73fe72967da13aa6b76442bd8eee41717)) +* **orchestration-service:** integration tests ([4160197](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/41601972a39148d45cb68591bdcb80ec3ea1304a)) +* **orchestration-service:** integration tests ([78a28dd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/78a28dd47f66949e50df684e05c26e2f0ffbc0a2)) + +## [4.3.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.3.1...v4.3.2) (2025-02-11) + + +### Bug Fixes + +* **prompt-registry-notebook:** remove api-url in ([ced95f4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ced95f4699bf5013a3962b261c57a6b149ef50c5)) +* **prompt-registry-notebook:** remove api-url in ([7fc9ee5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7fc9ee506fe94cbe718303371d517760e7f73d51)) + +## [4.3.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.3.0...v4.3.1) (2025-02-04) + + +### Bug Fixes + +* **metadata:** fix required metadata ([28243a3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/28243a38886329841cc311a1d090b5b4a011c21d)) + +# [4.3.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.2.3...v4.3.0) (2025-02-04) + + +### Features + +* **orchestration-service:** grounding module AIWDF-2218 ([6f6bd06](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f6bd06acb8723cefcf0773462497d8a874267a6)) +* **orchestration-service:** grounding module AIWDF-2218 ([4b93156](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4b931563ac6526245362aed8bd9841e3263dfdc1)) + + +### Reverts + +* Revert "replace gpt-3.5" ([8db8bf8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8db8bf860426c4fafa38b5de922e178b0e1a4196)) + +## [4.2.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.2.2...v4.2.3) (2025-02-03) + + +### Bug Fixes + +* **orchestration-service:** make schema a mandatory parameter and omit description if not provided ([0a6b837](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0a6b837ddec3db8b8dc95d2c7d6f01cdbc5170f0)) + +## [4.2.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.2.1...v4.2.2) (2025-01-31) + + +### Bug Fixes + +* **openai-client:** fix typo ([f21cc6b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f21cc6bcd42a12b40c24c7751940613bf3ac7bf0)) +* **openai-client:** force passing of max_tokens for older gpt models ([09df539](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/09df5397fa03f1d6c5d61745a453f60cf41542ed)) + +## [4.2.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.2.0...v4.2.1) (2025-01-29) + + +### Bug Fixes + +* **doc:** trigger main version build ([7176c1b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7176c1b9a34ee4c277809bda906096eedf0c138c)) + +# [4.2.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.1.2...v4.2.0) (2025-01-28) + + +### Bug Fixes + +* **docs:** add dark mode support for Jupyter cells ([bd6bc84](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bd6bc84793b756e82f655d3d0f315652045b1262)) +* **pylint:** add unit test ([1d5eece](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1d5eeceb3ac06506e6757c5a4dc86e107a95227a)) +* **pylint:** refactoring ([8972726](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/89727269c8082e262e2c666515c851f1d8a7e1ca)) +* **pylint:** refactoring ([d54ffce](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d54ffce161d1f4ab570fc2ffabf4c562e5790cb2)) + + +### Features + +* **orchestration:** add documentation ([bb9d51b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bb9d51bb1aff15df41aefc731570b1c636345bca)) +* **orchestration:** add factory class for mapping ([00d50bc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/00d50bc9923f4a18449f3f895bc90d898817f829)) +* **orchestration:** add integration tests ([1aedddd](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1aeddddb5ffc227f1be785738925a98ec2bc5d90)) +* **orchestration:** add json schema ([cb79eec](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cb79eec9de5af0d0d62637e299188c65cb4fb418)) +* **orchestration:** add json schema integration test ([d5a40b1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d5a40b19b12d24c5544181d635facc9911264a01)) +* **orchestration:** add minimal json schema exmaple ([3b83316](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3b83316e6b7ab816d476d509d97e9185ceb3ede5)) +* **orchestration:** add response format parameter ([618ffe6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/618ffe6d3854b120055723baae07f2fa6061c1e5)) +* **orchestration:** add strict integration test ([bc5e6a7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bc5e6a7b629179e04bab660469e6b50ac9cd796c)) +* **orchestration:** add unit tests ([3d30b87](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3d30b871bd28c6d18ff0f7f5e65d69d6f43f782a)) +* **orchestration:** add unit tests for name validation ([8e681a5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8e681a569f48bce283a18dbd7103dc83fe8d842c)) +* **orchestration:** adjust prompt for json_object integration test ([adfdc9f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/adfdc9f34ec4acf2283fbf07a149ff43468103e1)) +* **orchestration:** fix sonar qube issues ([d5ff82f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d5ff82f7572f6551e538bfd1b9a162fd0410a3a0)) +* **orchestration:** new response format in templating module for AIWDF-2339 ([e0ddb46](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e0ddb4667a0c1cfa16f7115190a224778d70318c)) +* **orchestration:** refactoring ([587ee88](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/587ee886b3267464a69a2f106a2dd22a2f8ec212)) +* **orchestration:** refactoring ([66b8e6e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/66b8e6ee29e0e64823cbd2a9e25a4220e8587d4a)) +* **orchestration:** remove import ([d58066c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d58066c4e2f562185283625af6e10cfe159911d0)) + +## [4.1.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.1.1...v4.1.2) (2025-01-20) + + +### Bug Fixes + +* **ai-core-sdk:** update ai-core-sdk ([f27d71f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f27d71f4638db50f8ed452f813830ef56d5af640)) +* **docs:** disable -- are being converted ([763ef2f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/763ef2f6b32d65699eea1f3decd9d1a360ecbbd0)) +* **test:** fix test ([5c4e9f0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5c4e9f04cea31844892045d284e11c1cc6385759)) + +## [4.1.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.1.0...v4.1.1) (2024-12-09) + + +### Bug Fixes + +* **docu:** provide documentation for 2412a release ([b6d3845](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b6d3845f139d1e3be49337784f08e69666c83b03)) + +# [4.1.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v4.0.0...v4.1.0) (2024-12-09) + + +### Features + +* **orchsetration-service:** prompt registry templates AIWDF-2085 ([1ff5b36](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1ff5b367b7d847c9765e012736113b99de0bc0a2)) + +# [4.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.8.3...v4.0.0) (2024-11-21) + + +### Bug Fixes + +* **deps:** update ai-core-sdk ([8e9d5a4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8e9d5a42540b0cfbdab49adee2370ce2d95ee1a4)) +* **openai:** Increase max_retries default from 0 to 10 ([227c7c1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/227c7c1214369080e0b22ca78b22a576fb84a389)) +* **openai:** Set max_retries default to 2 ([814995d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/814995d5a63117dbce5c50aaf1bbb305b2571916)) +* **pydantic:** replace usage of old pydantic ([942f8da](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/942f8da8c42345e186af255ad456ad8e6c5a3e4a)) +* **unit-test:** add proxy_client to amazon test ([69b907f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/69b907f7dcc408ae92a7c2a7826eff58fec74648)) +* **unit-test:** downgrade google-cloud-aiplatform dependency ([51b189a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/51b189a4f5c6b5f19fcabd1697b4631e0253a2cb)) +* **unittest:** Due to pydantic version ([a382e55](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a382e55cd2510b21dbb78c5af8bfb7f2b68b6dd7)) +* correct type casting for streaming ([d4abc2a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d4abc2aad21bfb1a5149008093378e74b62f6d41)) + + +### Features + +* **orchestration-service:** add streaming of orchestration requests ([ff6934e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ff6934ea2c1709bab0e5286f5f001ca6508401a4)) + + +* fix(langchain)!: upgrade to langchain 0.3.x ([8ef82e1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8ef82e1bce281e97dd14ce865e10f7e1ef25cf91)) + + +### BREAKING CHANGES + +* upgrade langchain and dependent libraries to 0.3.x + +## [3.8.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.8.2...v3.8.3) (2024-11-13) + + +### Bug Fixes + +* **deps:** update dependency langchain-community to v0.2.19 [security] ([57ff51f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/57ff51f918b9bcd04fb8a9cdaa7accdca7f1a6a9)) + +## [3.8.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.8.1...v3.8.2) (2024-11-12) + + +### Bug Fixes + +* **checkmarx:** fix one checkmarx error with new theme ([a9ac7a1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a9ac7a1d20709f9d4b36a0de93c73fb9d7cf5e50)) + +## [3.8.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.8.0...v3.8.1) (2024-11-05) + + +### Bug Fixes + +* **requirements:** loosen packaging dependency ([1b4e193](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1b4e1931be153f1e3938f922b8f34b7f743304bd)) + +# [3.8.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.7.0...v3.8.0) (2024-11-04) + + +### Bug Fixes + +* **sphinx:** external documentation link for pypi ([0ad688d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0ad688d92d9e7a07a56e0ffd02be827471244861)) + + +### Features + +* **docs:** comment out lines in javascript ([772d914](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/772d914a9e78ea810dd4f674f5a0e81255560be5)) + +# [3.7.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.6.0...v3.7.0) (2024-10-29) + + +### Bug Fixes + +* **doc:** correct the README ([b6d2028](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b6d202895d11534eb90042c8f4d872f4b288d087)) +* **docs:** fix examples ([6f7ded5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f7ded5194478b0363e9d300a98b0a992c706ac4)) +* **example:** addressed code reviews ([d6fbd08](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d6fbd08a83b0d5b4e3f029e53545f8b4c335a084)) +* **README:** update README ([a8baf01](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a8baf01c72806335810ccbd6dd5be60322952f73)) +* **tests:** add tests for custom models with native clients ([0f62abf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0f62abfeb8522e985da91660152b18c82df0cc33)) +* **tests:** fix integration tests ([2aff868](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2aff86812d092be5290ace6712449849dcb05fab)) +* **tests:** fix integration tests ([dd20faf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dd20faf9f8bf292b95a6e5f984a37c2a7067306e)) +* **tests:** fix integration tests ([7f511ad](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7f511ad5cbb1e52960e001f363773318eed3be71)) +* **tests:** remove draft leftovers ([2bf75db](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2bf75db48fecfc119349bd5873d0aea70bbcc86c)) +* **tests:** rename test ([6b1ca4a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6b1ca4aaaac59826c7bf70bcb039d1aacdf5bcce)) +* **utils:** fix prediction urls ([2f30297](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2f30297c8c02692eb6c0416f36809aa20101857f)) + + +### Features + +* **custom-models:** support custom models ([692bf65](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/692bf65900ab40bd7ffd8404d6c97f3cfbaf6add)) + +# [3.6.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.5.0...v3.6.0) (2024-10-24) + + +### Bug Fixes + +* **bug:** add exception if model or executable not available ([4fed137](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4fed13748e9fff87090c557376bfd03fc292c5d7)) +* **bug:** AttributeError: NoneType object has no attribute id_ ([f15185e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f15185e75ce1753b8dee0550b025f1a02017d66d)) +* **bug:** configure program flow ([54f62b1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/54f62b11a0d7cb6d66a742d2dfd74d538b368dae)) +* **bug:** remove test model ([f2d0b1d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f2d0b1d72823874d3884c46c481e5595d7cd4309)) + + +### Features + +* **doc:** adjust index-url and trusted-host ([8afa8e2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8afa8e24460a43fef0442c302612cf2b4310dd0a)) +* **doc:** document the framework ([728817b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/728817bdf8adcdd2ebe08d892021827e91ff6f5c)) + +# [3.5.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.4.0...v3.5.0) (2024-10-24) + + +### Bug Fixes + +* **deps:** update ai-core-sdk ([9f1ded3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9f1ded376817df3d403467fbd9e8c9261a96fcba)) + + +### Features + +* **doc:** add preview_html step in makefile ([f0f481b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f0f481b6db504614a8a49778598117cad865d2dd)) + +# [3.4.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.3.0...v3.4.0) (2024-10-24) + + +### Features + +* **doc:** add files to structure ([2116a57](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2116a5755e70ff855888bccad77d76ad6375e548)) +* **doc:** add first api docstrings ([e6b1e89](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e6b1e8946bb19bcc5deb0de2cd25881bc96f8d7a)) +* **doc:** Add github workflow for internal documentation ([e7d45b8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e7d45b8f8d22f2aca182b9585ddaf6bb2d48a722)) +* **doc:** add mkdir to workflow ([6d89d35](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6d89d3500c8874acc05b033f5a777f0283f29c23)) +* **doc:** add pypidescription ([4918f8f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4918f8f3eeccaf1c299227099a970ee057e3fd1c)) +* **doc:** add readme.md ([9c57c6b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9c57c6b9bae8e681a6ada80fac60d77873908716)) +* **doc:** add requirement installation ([096eedb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/096eedbefa963c5901234bdc5e59e2a1e566fa45)) +* **doc:** add test for sap help ([2116058](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2116058107b1532a511783544428e749eebf7c18)) +* **doc:** adjust title ([007771a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/007771adc3712f5aaa465ea9144f6ff2ba9739a2)) +* **doc:** adjust workflow ([33d8264](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/33d8264d38e1990e766ace748afe260bfd8f0051)) +* **doc:** adjust workflow ([d9d1863](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d9d1863d8ae18182f7cd26c7bc868f1cc93941cc)) +* **doc:** change branch to main ([a9d191a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a9d191a7904b011bc5791d06483df3538a7a534f)) +* **doc:** change branch to main ([caad963](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/caad9635f0967fee941bc701c4eccfe2904b88ae)) +* **doc:** change branch to main ([662495f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/662495fb22184ad4d88b24745a2ce3b105feb649)) +* **doc:** change python and sphinx version and add release notes ([51d2f71](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/51d2f7139a041a84134833ab4e0ba2956d1cbff4)) +* **doc:** copy files into source folder for generation ([808d9e6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/808d9e67a62c41c180e0f588310b09c11ce3cc62)) +* **doc:** generate html ([2a02140](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2a021403c02161cdead72e1a2fe8ae55ea629937)) +* **doc:** introduce sphinx documentation tool ([b3803a8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b3803a83dc0f48866ecb9bed740f8e2950095168)) +* **doc:** new navigation caption ([7e439c4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7e439c422f8a02a57949d70a2e2c482545896471)) +* **doc:** new readme for introduction ([a478a18](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a478a18a1144ef92b78ba1ca8967bd03eb491bc5)) +* **doc:** new script for copy of files and copy feature ([6e94e34](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6e94e348c05aa9e5a3c1156c0cdd9dd681e151e3)) +* **doc:** new structure ([2045c03](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2045c03e74675d7fd14073025db4a7306e912d51)) +* **doc:** remove blank line in file ([5fec4ef](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5fec4ef2590c6d68256c003ca6bebe66b6079b55)) +* **doc:** remove sphinx extensions and rename structure ([82bdfce](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/82bdfcec45f1a88e1cee5ab7dface425df9a17a6)) +* **doc:** rename of folder ([7e493b0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7e493b04d5c86fb7a111d17f19ecd04189a6543f)) +* **doc:** test change for generation ([8925757](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8925757938a5617c1f9af663d56e0c56017cdeb1)) +* **doc:** test change for generation ([f9942c8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f9942c8166df322d375408f18297f0b225ddd93e)) + +# [3.3.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.9...v3.3.0) (2024-10-23) + + +### Features + +* **model:** mistral large instruct ([da867aa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/da867aa7318232eac4741c58794b28b4c90a89f0)) +* **model:** register mistral large ([3f0a15e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3f0a15eea4354955735be2143eeb758201c2c24b)) +* **model:** remove comma ([6e8bf4b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6e8bf4b36688081e97337859b0a132912857508f)) +* **model:** remove mistral large for testing purpose' ([00b1e8d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/00b1e8df7c57aa3845bd597af4d1c49568ee5bd6)) + +## [3.2.9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.8...v3.2.9) (2024-10-23) + + +### Bug Fixes + +* **orchestration:** rename module result field for data unmasking ([36df532](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/36df5325846ac7777c84e17b7f109b08a1aba5c9)) + +## [3.2.8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.7...v3.2.8) (2024-10-18) + + +### Bug Fixes + +* **model_init:** harmonize model lists ([6f8fd5f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f8fd5fbbfa20136ea7a766493a18934770bd610)) + +## [3.2.7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.6...v3.2.7) (2024-10-08) + + +### Bug Fixes + +* **checkmarx:** switch to checkmarx one ([beb766c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/beb766c4d5c6daae0d0f9188376928b77344d569)) + +## [3.2.6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.5...v3.2.6) (2024-09-26) + + +### Bug Fixes + +* **integration-tests:** change RG to gen-ai-hub-sdk ([37a5e27](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/37a5e27e87b226366ed7ab995d698bf23657df90)) +* **integration-tests:** remove dummy change ([011241b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/011241bf5c6a17762479dea622cef084f401e99d)) + +## [3.2.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.4...v3.2.5) (2024-09-25) + + +### Bug Fixes + +* **integration-tests:** Safeguard against hyperscaler content filters - Other ([c0b3a42](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c0b3a421d0277450fc1dcc8384b691aa5e52dcc5)) + +## [3.2.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.3...v3.2.4) (2024-09-25) + + +### Bug Fixes + +* **integration-tests:** add temperature and seed to openai ([dc30176](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dc3017626debc7861197fd3289c4e64203918505)) +* **integration-tests:** Do queries with temperature=0 to safeguard against hyperscaler content filter ([d1cd82a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d1cd82ae8b38eae416f8dc3bad449fe2b823177e)) +* **integration-tests:** introduce temperature and seed ([3e7c781](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3e7c7814ce56034872d016671a6b43eac0060c1d)) +* **integration-tests:** remove retry from mixtral and llama ([7ef850e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7ef850e05686ac312ea29f22cdfef880640d2c38)) +* **integration-tests:** remove temperature and check unit tests. ([16ca189](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/16ca18927ef4cf213e1588b382db0e1bfa8650c5)) + +## [3.2.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.2...v3.2.3) (2024-09-19) + + +### Bug Fixes + +* **integration-tests:** adjust max_retries from 5 to 20." ([b123b67](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b123b67b22d1d4027b42af81ec2c0237b436b073)) +* **integration-tests:** adjust max_retries to 10 ([f97f680](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f97f6801c3178d0ccd21067efce6b8acec6fde87)) + +## [3.2.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.1...v3.2.2) (2024-09-18) + + +### Bug Fixes + +* **integration-tests:** add max_retries to all openai multimodel tests ([2c4e795](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2c4e795121ffb0f6f0dd7cfc4e7cffcfadd42532)) +* **integration-tests:** add max_retries to other openai client calls ([6b47124](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6b4712497ab3fc7d2991107c309a6538136c8bf4)) +* **integration-tests:** increase max retries. ([3674bf6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3674bf6a0d9b567231b0674aeb3dfb121f3c7a41)) +* **integration-tests:** increase max_retries to 4 ([a059384](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a05938491ce76dba91fcb1c4ff76d987ff840afc)) +* **integration-tests:** pipeline test ([efa465f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/efa465f4333447221b59b44667ab1a2eaf1a93c0)) +* **integration-tests:** remove test ([55f5b75](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/55f5b75dea71b803f94404fbeca8a314902ece04)) +* **integration-tests:** try max retries for openai ([cf49650](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cf49650804fe96e2cb8d4c1d88469696da7aa1ff)) + +## [3.2.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.2.0...v3.2.1) (2024-09-17) + + +### Bug Fixes + +* **tests:** remove reduntant test ([9b292a7](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9b292a74775e24fe0ae10d4c8c3992af97fdf336)) +* **x509:** upgrade ai-core-sdk ([5ff2f26](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5ff2f26fb7b61f0a97ac90559a3f0bf365ae798d)) + +# [3.2.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.1.1...v3.2.0) (2024-09-17) + + +### Bug Fixes + +* **docs:** update placeholder values ([451f11e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/451f11eae691525e46825947598c7a8aebe21b4f)) +* **orchestration-service:** ensure backward compatibility with content filter refactor ([b79dadf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b79dadf8209ac5b399c5208f46159eb93ce0f9d4)) +* **orchestration-service:** improve orchestration call error handling ([ad8d266](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ad8d26659909c3a3f4c2ee0c7b35deaa8c6e965a)) + + +### Features + +* **orchestration-service:** add support for data masking ([de9991a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/de9991a0478d35b0ff5dd4977846b98e6f3fe44c)) +* **orchestration-service:** explicitly define Azure content filtering threshold settings ([6c6b662](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6c6b6622ba8c9c12ede4a62cf720d944e1efbf5e)) + +## [3.1.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.1.0...v3.1.1) (2024-09-09) + + +### Bug Fixes + +* **model-discovery:** maintain compatibility and prevent breaking changes ([b6da239](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b6da239cc88e3411577179c4492500eaa36b060e)) + + +### Performance Improvements + +* **model-discovery:** concurrently fetch configurations for models ([d14ddfc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d14ddfc8fc809c85f65fd4f5d2b84ec64457a43c)) +* **model-discovery:** lazy load models when deployment_id is specified ([19c430c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/19c430cd2d33edfa12aa750ac0be5cbb8d459399)) + +# [3.1.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v3.0.0...v3.1.0) (2024-08-29) + + +### Bug Fixes + +* **integration-tests:** remove falcon model ([1aaaf57](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1aaaf5715a9fa2172e858b4161ea3407c8f3a602)) +* **integration-tests:** remove falcon model from client ([2745801](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2745801aab5b0be26ebfc1ea7ea8b82592cbca2d)) +* **integration-tests:** remove falcon model from discovery ([2f01a71](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2f01a71284a9edbdc6c8278f350889c3ce40ccac)) +* **orchestration-service:** correct UUID usage in configuration name generation ([f798784](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f7987844692a215acee1ef102733b337852d3e14)) +* **orchestration-service:** provision test deployment ([34ba2a6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/34ba2a663c1ebe8fb3b027ebbdef30ce5fcecad6)) +* **orchestration-service:** remove restrictions from base classes ([738c279](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/738c2793ea7ae3a9e38ff136c4219733f8293750)) +* **orchestration-service:** response data of filtering is optional ([aa32b0d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/aa32b0d532e107a86a65215cbf9804b26de99440)) +* **orchestration-service:** update chat bot example with correct history ([6019f44](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6019f445056faf2ffd50b1d457b4f5862b8ef996)) +* **orchestration-service:** use mocked proxy client for unit testing ([5278ea2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5278ea27e45ee7be2f5113c04f849c23aef4c24e)) +* **orchestration-service:** use shared deployment for integration tests ([7905528](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/79055287a81f332396c48f9781f36eeb63a8555c)) +* **requirements:** update ai core SDK dependency ([1d3fd45](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1d3fd4583d5440cf797b1ec52f5a262f97ab7008)) + + +### Features + +* **orchestration-service:** add dacite as requirement ([582cbe3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/582cbe35b1696c05dfe17d757de7d27d407495ef)) +* **orchestration-service:** add integration tests ([1668f28](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1668f28951592934515da84ec29c45afeb2121b4)) +* **orchestration-service:** add support for templating, LLM, and content filtering ([07ba232](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/07ba232716438009c8ec26bba9c1f30a82d5f276)) +* **orchestration-service:** add unit tests ([458a88e](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/458a88e40d1f582f3338c9a9c8df7aaf06f83536)) +* **orchestration-service:** change from to_json to to_dict ([1930b59](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1930b598008eb878b4e6af7310ab973d419564df)) + +# [3.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v2.1.1...v3.0.0) (2024-08-21) + + +### Bug Fixes + +* **blackduck-docker-image:** Adds missing library for building dependencies ([592d247](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/592d2479aa7ad9b938f990fe0f09a3966b14514f)) +* **deps:** pin langchain-community to last compatible version ([2bfd94f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2bfd94f8bc6b5ac3308f442025bfe0644a15c236)) + + +### Features + +* **model-support:** integrate Anthropic Claude 3.5 Sonnet model ([53e37b4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/53e37b4f9a5450da8c4c4e3d89882f01e525d326)) +* **vertexai-support:** Adds native support for Google vertexai SDK ([5e3e4bb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5e3e4bb8fd56f44f743de31aaf8ac72ab8e12fcc)) + + +* feat(vertexai-support)!: Adds native support for Google vertexai SDK ([bf69ced](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bf69cedfd18c91b4d256b50177d04ec760d670a4)) + + +### BREAKING CHANGES + +* Removes Google Gemini SDK Integration and changes install extras + +## [2.1.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v2.1.0...v2.1.1) (2024-08-05) + + +### Bug Fixes + +* **dependencies:** update ai core SDK dependency ([7eafbda](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7eafbdabf98842db56d59c01d1fb41258418d5ae)) + +# [2.1.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v2.0.1...v2.1.0) (2024-08-05) + + +### Bug Fixes + +* **bedrock:** undo changes regarding converse API ([f79a616](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f79a6162d10d6ccc224c0f6c1d8ab27db16af612)) +* **langchain:** add init_embedding_model support for Titan ([9388a7a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9388a7ad4efa42efe86384470a5c8e7456b922a0)) +* **langchain:** add init_llm support for Amazon Bedrock models ([72777a6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/72777a64290f463ec574df513f5153557bfa11d3)) +* **test:** adjust LLMChain stream test for dictionary-based output format ([d9991b9](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d9991b9d1319dce548577fe3ec6acf11dd5ad851)) +* **unit-tests:** adapt to changes in streaming integration ([4da0b25](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4da0b259d781d600084bcf4252708febf8cf4a87)) +* **unit-tests:** tests custom Gemini streaming response iterator ([699226b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/699226be9c468250a6878fa4ffe9dadf510d42e2)) + + +### Features + +* **bedrock:** enable streaming with AI Core proxy ([fb32191](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fb321911f8008f3b9c233cec3cff2780cca06ce8)) +* **google:** enable streaming with AI Core proxy ([7e1c97a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7e1c97ab07b36ebd8017331b84b8ae79777b7741)) + +## [2.0.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v2.0.0...v2.0.1) (2024-08-05) + + +### Bug Fixes + +* **blackduck:** enable signature scan ([bf678e1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/bf678e12962906e7c046476ab71e0fca322296b8)) +* **blackduck:** set java env in docker ([7a3b931](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7a3b931442d99b8624adbd2c67370fed5b7563d2)) + +# [2.0.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.11.2...v2.0.0) (2024-07-26) + + +### Bug Fixes + +* **requirements:** Changes package extras. ([67af990](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/67af9904c31d40bdbba0898967ba812dd781a89e)) + + +### BREAKING CHANGES + +* **requirements:** Install extras differ from earlier versions. + +## [1.11.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.11.1...v1.11.2) (2024-07-25) + + +### Bug Fixes + +* **blackduck:** activate signature scan ([3ee468f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3ee468f00ce150f19edad7b56d16f802bd7dd427)) +* **blackduck:** deactivate signature ([22a63b6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/22a63b65d08b9ebc2469b9ebd3055c3cb49683bc)) +* **google-gemini-langchain-upgrade-0.2.x:** Fixes upgrade issue by refactoring google gemini integration ([8e2eceb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8e2eceb3aba7777b92e8e55adb86038b1a985b67)) + +## [1.11.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.11.0...v1.11.1) (2024-07-05) + + +### Bug Fixes + +* update requirements.txt ([ffa57e0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ffa57e0a40ef5cd7110a64f1852179653524eb59)) + +# [1.11.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.10.1...v1.11.0) (2024-07-05) + + +### Features + +* **model-support:** Adds support for amazon--titan-embed-text ([a30c2f6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a30c2f62042b0b2f85ba510ee3b889f2c186e2b7)) + +## [1.10.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.10.0...v1.10.1) (2024-07-02) + + +### Bug Fixes + +* **gemini:** fix typo ([7daad9f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7daad9fb05363f1f93496f9b9003768b279c8934)) +* **gemini:** reorder models list ([e8ef645](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e8ef645125c9b5143f4060edd983f6515365d869)) +* **gemini:** reorder models list ([93a5ae5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/93a5ae535a0148372b184856602a9c9c03bab58d)) +* **gemini:** support new gemini models ([ddcc8d2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ddcc8d2b639335c4653cdbb68872eee0c42c4f37)) + +# [1.10.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.6...v1.10.0) (2024-07-02) + + +### Features + +* **model-support:** Adds support for anthropic--claude-3-opus ([f503c8d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/f503c8d9dae0bd7def40906cb799913099dda0cb)) + +## [1.9.6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.5...v1.9.6) (2024-06-25) + + +### Bug Fixes + +* **renovate:** improves renovate config ([c51c84b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c51c84bf77b8d82be4ba5b391a27e92df0c908cb)) + +## [1.9.5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.4...v1.9.5) (2024-06-20) + + +### Bug Fixes + +* **requirements:** update core-sdk ([25619b3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/25619b38834f244308410ec3dc44fd75723b7139)) + +## [1.9.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.3...v1.9.4) (2024-06-20) + + +### Bug Fixes + +* **requirements:** update core-sdk ([e001925](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e001925d76c468f5590cb8c768ab2e7ba61933a3)) + +## [1.9.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.2...v1.9.3) (2024-06-20) + + +### Bug Fixes + +* **cleanup:** switch the order ([58d7ede](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/58d7ede792d7378984f5318a00a77cea4da79ce8)) +* **openai:** support gpt 4o model ([1b323fe](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/1b323fe56000dd49f21704ae1eb5e5869f2da97f)) + +## [1.9.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.1...v1.9.2) (2024-06-18) + + +### Bug Fixes + +* enable gpt-35-turbo-0125 for init model ([948114d](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/948114d0cf31b445674934c13bd0c23fb48c52fd)) + +## [1.9.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.9.0...v1.9.1) (2024-06-17) + + +### Bug Fixes + +* enable gpt-35-turbo-0125 for init model ([0e3ba34](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0e3ba349cee5a35fe790e73d1baa0a53e72ea375)) + +# [1.9.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.8.1...v1.9.0) (2024-06-13) + + +### Features + +* **model-support:** Adds support for anthropic models (claude-sonnet, claude-haiku) ([5af6ba2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/5af6ba2fb9ee2bef26094d30dedc7960c1544912)) + +## [1.8.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.8.0...v1.8.1) (2024-06-12) + + +### Bug Fixes + +* **blackduck:** adjust config for major release setting ([6f83e40](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f83e403b290f8011bb5e47db01aff75d18a9cab)) + +# [1.8.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.7.0...v1.8.0) (2024-06-12) + + +### Bug Fixes + +* **pylint:** Adds pylint exception for dynamically added memebers ([860219a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/860219a5ece1c91e3458d2b067452a9d8ff1d174)) +* **setup:** fix pkg-info ([658e5b2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/658e5b22ec1915606a4565c3b0c52d44aa8d6a9c)) +* **test-coverage:** Adds unit test for amazon streaming methods ([0dd935c](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0dd935c8860a6d70c5d32f9c06223959bf5c33b8)) + + +### Features + +* **mistralai:** Adds mistralai--mixtral-8x7b-instruct-v01 support ([e5f308f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e5f308fb48ca38098005baa40468a1bcec8be602)) +* **model-support:** adds langchain support for amazon--titan-text-express model ([fbb9aae](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/fbb9aae2c5aa560c88c4b23a208f489a64ce99c3)) +* **model-support:** Adds support for amazon--titan-text-express ([145aa63](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/145aa631c57655a90655e634ed93e7d2bf3947e4)) +* **model-support:** Adds support for amazon--titan-text-lite ([ab827f3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ab827f3a84bd2a042d24a460419f4c2f0f33a123)) + +# [1.7.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.6.2...v1.7.0) (2024-06-11) + + +### Bug Fixes + +* **doc:** format ([02b900b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/02b900b2bbcbbd9b1f2e025e743611097de17508)) +* **doc:** update readme and example doc ([69c4b10](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/69c4b1045d1472200bc9f7d1c4a980ca2192ede0)) +* **langchain:** add langchain changes ([5456631](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/54566319506aa0044b8c90f3d6823c4b50b6aaad)) + + +### Features + +* **meta-llama:** provide meta-llama support ([d761fb6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d761fb6d99a2aca5084575239ae928686e2b65f9)) + +## [1.6.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.6.1...v1.6.2) (2024-06-06) + + +### Bug Fixes + +* **langchain:** cleanup ([6f23522](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f2352233f8696bd1fb64bbcd3cba23330391db4)) + +## [1.6.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.6.0...v1.6.1) (2024-06-05) + + +### Bug Fixes + +* **requirements:** update core sdk ([a99b8cb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a99b8cb756192607d913f50030cb8ed31ff56b3f)) + +# [1.6.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.5.1...v1.6.0) (2024-06-04) + + +### Bug Fixes + +* **blackduck:** add blackduck to cumulus ([ddd3e36](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ddd3e3635e0453556c45fd6615531bf09bd95a58)) + + +### Features + +* **model-support:** Adds support for text-embedding-3-small and text-embedding-3-large ([08e2165](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/08e2165644410f2a23eb46d35149551c2e1a3dd0)) + +## [1.5.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.5.0...v1.5.1) (2024-05-23) + + +### Bug Fixes + +* **doc:** add Gemini examples ([c728c93](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c728c9327cdcf34f5f7cf3f9ccc1b4666dbc97a6)) + +# [1.5.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.4.0...v1.5.0) (2024-05-23) + + +### Bug Fixes + +* **google:** cleanup ([668cc39](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/668cc39d27e2aaaea6384564de8591670387d5ec)) +* **import:** fix pylint ([dbda817](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/dbda817847ed4298906204176ef7c02784631848)) +* **init:** add googlegenai import ([9e3841f](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/9e3841f85a753b5a2b3a87b0078ad4ac380fa4b4)) +* **init:** move import ([6a2f256](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6a2f2568731d9505600ef9b8d1c97874d7e2e988)) +* **requirements:** update requirements ([0c5588b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0c5588b98f821a2a8e56a1e2d49c725a85cca6be)) +* **test:** add unit test ([4457a64](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/4457a64a1607c17138945fe923c7190a9c7bc28a)) +* **test:** rename mock response ([004982a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/004982a24f8ea44289ae5b2788b8702d6e3cb2d2)) +* **test:** select default model ([93c60f1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/93c60f156260ad9613c01554e8c406b5c87f25b2)) + + +### Features + +* **gemini-langchain:** add langchain support for gemini ([61374bc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/61374bc1d1956d7d92579f6e3d47c645fe1078ee)) + +# [1.4.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.3.3...v1.4.0) (2024-05-21) + + +### Bug Fixes + +* **doc:** fixed doc for external ([aef38a2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/aef38a24dcce088909498ff87e86b0e24a223dd6)) + + +### Features + +* **X509:** added docs for X.509 support ([59ce917](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/59ce917e8089e4fa3105dbc3211cb19113e7e56b)) + +## [1.3.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.3.2...v1.3.3) (2024-05-16) + + +### Bug Fixes + +* **client:** fix typo ([2e6e1b2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/2e6e1b24f586ce684587239b3dd82fbb302a1084)) +* **client:** remove stream subclass ([19e3c1b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/19e3c1b05226f78fa1206403d5939d9247311f59)) +* **client:** remove unsupported models ([cd1f6f4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/cd1f6f4b5676f8e8e1f9da6d76338eef922e9445)) +* **google:** remove langchain ([55af3b4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/55af3b4bf766f368f564ca6892af99f7370b1d99)) +* **requirements:** remove unused ([d2da8ad](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d2da8ad3b5906b22214199ecb22cfdbf62778be7)) +* **test:** add unit test for gemini ([e946d53](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e946d53b463369fa3f5eaff9892f8f1e9716572d)) +* **test:** fix tests ([498aca4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/498aca43df79442d4365d1ab4b61b5855bd4dd91)) +* **tests:** add tests ([ed0bbe4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ed0bbe457b1555bd14315865d4147388495f3231)) +* **tests:** remove google langchain test ([46e8d76](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/46e8d768c059afea2ba594ebd280299a209a24d1)) +* **tests:** remove unused ([60aa7fa](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/60aa7facafcd3ac3e4ea9239d8fd35dcb25af5b7)) +* **tests:** remove unused ([e772fdc](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e772fdc383385d834668f3ee270434d1b67fb561)) +* **tests:** test deployments ([0011b99](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/0011b99497f23fa11a146e739afc3fc945f4787c)) + +## [1.3.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.3.1...v1.3.2) (2024-05-07) + + +### Bug Fixes + +* **token:** remove caching ([e86ada2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e86ada2e87f1164251e191b028f1170ee014dae8)) + +## [1.3.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.3.0...v1.3.1) (2024-04-25) + + +### Bug Fixes + +* **url:** add new gpt model ([e1cdf72](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/e1cdf723e69655b8cb4c5078eb99caad76727595)) + +# [1.3.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.2.4...v1.3.0) (2024-04-03) + + +### Bug Fixes + +* more graceful handling of non-LLM deployments ([07694eb](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/07694eb7c616b638db5bcf32e045ae1b6ff7b4f7)) +* more graceful handling of non-LLM deployments ([3dd1bd6](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3dd1bd68c43407a219bc9b6a540f47ce3c3b608d)) +* more graceful handling of non-LLM deployments ([54876d5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/54876d530f7ab0fe0da4b37c00e26aef41978922)) +* more graceful handling of non-LLM deployments ([568281b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/568281bd9dc8d0207bfbb7ee07d78ddb87ee99cb)) + + +### Features + +* optionally install langchain ([6f48cf8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/6f48cf82ee591d6b1e77784b50359e51ddf19102)) + +## [1.2.4](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.2.3...v1.2.4) (2024-02-21) + + +### Bug Fixes + +* Falcon model fixed in intprod ([210bfde](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/210bfdef7ba6fc3ef70c2cd7cc79a50d5b9b3a00)) + +## [1.2.3](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.2.2...v1.2.3) (2024-02-15) + + +### Bug Fixes + +* **test:** enable streaming test for chat completion ([94f2188](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/94f21889bec185de6a9a9dbcd4d9c3260301a627)) + +## [1.2.2](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.2.1...v1.2.2) (2024-02-06) + + +### Bug Fixes + +* Embeddings in example notebook ([ceaed90](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ceaed901c9e6de6f47ebcb80220ed832bcdcb878)) +* Embeddings in PyPi description ([ae03d76](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/ae03d76fbf47a06f8eedb6802e414ce457121bc9)) + +## [1.2.1](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.2.0...v1.2.1) (2024-02-06) + + +### Bug Fixes + +* bump version ([71b3678](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/71b36789a2de521123c5e683743c8ac95c99b2a2)) +* skip Falcon tests ([d91738b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/d91738bf84eb6a46ee2bf9d9048e2e2ec7590677)) + +# [1.2.0](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.23...v1.2.0) (2024-02-02) + + +### Features + +* release new version and fix PyPI error ([35b97c8](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/35b97c81582afb7e06abf3f994be69d5f16780dd)) + +## [1.1.23](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.22...v1.1.23) (2024-02-01) + + +### Bug Fixes + +* **cumulus:** adjust to correct Jenkins secret name ([b7fde3a](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/b7fde3ac176ecc5f8fea3866c1676483befbbaa6)) + +## [1.1.22](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.21...v1.1.22) (2024-01-31) + + +### Bug Fixes + +* **main:** trigger xmake build ([3d2e846](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3d2e846cbb5234666bd3a1ae9be0ad6864de8f16)) + +## [1.1.21](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.20...v1.1.21) (2024-01-30) + + +### Bug Fixes + +* **readme:** min change to trigger main build ([22a8f31](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/22a8f313913154dd1716a540c1ddc49564cab608)) + +## [1.1.20](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.19...v1.1.20) (2024-01-30) + + +### Bug Fixes + +* **doc:** dummy commit ([34d3776](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/34d37767e48d93a4b753183a16de5d6674d9ffe1)) +* PyPi description ([7ebafee](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/7ebafee7cac5582831caf4370db1a6592d24b8f8)) + +## [1.1.19](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.18...v1.1.19) (2024-01-30) + + +### Bug Fixes + +* **readme:** fix error in readme ([3751c55](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/3751c557aa59dbd4a58fdbbfead0cc0885c350f4)) + +## [1.1.18](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.17...v1.1.18) (2024-01-29) + + +### Bug Fixes + +* trigger release ([a6b85cf](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/a6b85cf5fd83af99e1aa62806877b49ce6354f8a)) + +## [1.1.17](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.16...v1.1.17) (2024-01-29) + + +### Bug Fixes + +* **config:** update the new env variables in vault ([45fdac5](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/45fdac51ec6bf1a8e6dc17f6850b4c7aee370279)) + +## [1.1.16](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.15...v1.1.16) (2024-01-29) + + +### Bug Fixes + +* **sonar:** add new sonar project name ([c175028](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/c175028ec3717e1d4e7fcb3f399e9667928d7000)) + +## [1.1.15](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/compare/v1.1.14...v1.1.15) (2024-01-29) + + +### Bug Fixes + +* **rename:** renaming cicd pipeline configurations ([8002e40](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/8002e40d9eccb21605d4a92b83080efb15b4a4e9)) +* **sonar:** revert to old sonar project key ([49f2021](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/49f20214d8cda95aa126e692d2be298d4ccd19e5)) +* **whitesource:** correct whitesource token ([468f14b](https://github.wdf.sap.corp/AI/generative-ai-hub-sdk/commit/468f14bda65e03f3aaf3fe22e72de4e09511c5cd)) + +## [1.1.14](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.13...v1.1.14) (2024-01-22) + + +### Bug Fixes + +* **proxy:** fix custom FM scenario functionality and minor cleanups ([cb8cc20](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/cb8cc2056f3b68ac85c9f1afa725582cb805f695)) + +## [1.1.13](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.12...v1.1.13) (2024-01-18) + + +### Bug Fixes + +* **notebook:** fix imports ([65ec3dc](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/65ec3dcee0c640eb440b95a0fdab6bbb0f8e9b75)) + +## [1.1.12](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.11...v1.1.12) (2024-01-11) + + +### Bug Fixes + +* **example:** update langchain method calls ([dc83247](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/dc83247bc11cecdf727fb866620844393f95445a)) +* **notebook:** update usage based on new changes ([b8ddbd8](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/b8ddbd8b09a52a16d65e068de9e68ac007e78eae)) + +## [1.1.11](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.10...v1.1.11) (2024-01-10) + + +### Bug Fixes + +* **test:** fix deployment discovery test ([e19d508](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/e19d5086af42fee109f572da7bd6f916f57f5a29)) + +## [1.1.10](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.9...v1.1.10) (2024-01-09) + + +### Bug Fixes + +* **manifest:** include correct files in final build ([1ee28c6](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/1ee28c63eda5b36033a4fda51870c0e15a8b0e69)) + +## [1.1.9](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.8...v1.1.9) (2024-01-08) + + +### Bug Fixes + +* **dummy:** release new version ([ad51cf4](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/ad51cf434dbe0c2540600c7bb8de7d5404ab3ad7)) + +## [1.1.8](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.7...v1.1.8) (2023-12-19) + + +### Bug Fixes + +* **test:** fix response of request with function call ([8c107ab](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/8c107ab5d805c4e753a17293fbc12f0c4583c180)) +* **tests:** fix async testing with pytest ([567eebc](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/567eebcc6ad48aedd247349540d40d28fdd9ac5e)) + +## [1.1.7](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.6...v1.1.7) (2023-12-18) + + +### Bug Fixes + +* **test:** add proxy engine tests ([548f3c6](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/548f3c6363be8a1aae7af9af40c12971374ee97b)) +* **test:** fix response of request with function call and skip for now ([6f75814](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/6f758145ea65ad9f5597a23f8be921cc3845356a)) + +## [1.1.6](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.5...v1.1.6) (2023-12-14) + + +### Bug Fixes + +* **cli-test:** changes based on review ([#33](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/issues/33)) ([e64528a](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/e64528ad2875b6771836d37ab2d386820e7b2ab4)) + +## [1.1.5](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.4...v1.1.5) (2023-12-13) + + +### Bug Fixes + +* fix summary ([736cca7](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/736cca78098f0d02e809ed6a601f2b1b2c9b14b6)) +* use the same way for metadata as in ai core sdk ([b45e2d8](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/b45e2d899caa0b031bc914593ead878770007996)) + +## [1.1.4](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.3...v1.1.4) (2023-12-13) + + +### Bug Fixes + +* **ai-core:** fix version dependency on aicore ([f022843](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/f02284395fc7d04be67de522d98264193e2e1166)) + +## [1.1.3](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.2...v1.1.3) (2023-12-13) + + +### Bug Fixes + +* **cli:** resourcegroup is already passed as a parameter ([e0e9bd4](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/e0e9bd47070e3b2d8e57e330aa8fe0a3bf0e439e)) +* **coverage:** fix coverage.xml and remove nosetest ([14cc697](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/14cc697a65a1db4d1a783dd1ac7c8391948e3710)) +* **depencencies:** update dependency versions ([ca72750](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/ca727502cbc0ab25dc44a2c1231b9bc1a23660b8)) +* **docs:** add documentation and pylint cleanup ([f7e8557](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/f7e8557b0256938f36b1e5003f057da193b5d208)) +* **duplication:** refactor init models ([f8725a5](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/f8725a50670a22b4619d52eb32f34083a7ebc7bb)) +* **duplication:** refactor langchain openai ([96bddd2](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/96bddd244332a107f228a91ba53270f685ad618c)) +* **init:** add missing modules ([3516a4a](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/3516a4a75764ef2f44453d1533528a47e0f86af5)) +* **param:** resourcegroup input is got from prompt ([a0b9590](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/a0b9590d4b9ecd913ed1efee9b88d3845a27a72a)) +* **pylint:** add pylint disable ([1aa49d2](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/1aa49d2fe8bb8ae083dbea8ecba1bff53a4f3820)) +* **pylint:** fix imports ([84e7b86](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/84e7b86149144d742ff0180caab8d2accc9d74b0)) +* **pylint:** fix pylint error ([998c879](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/998c879dd8e95bf4becffee1a0f9ef821f9c0a00)) +* **pylint:** fix pylint issue ([135064f](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/135064ffb0460cedbe9cbab2adfef4a09c16ba3d)) +* **readme:** fix typos ([8bf8442](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/8bf8442ee7fe938dff08a6ccd6fb42dae6c48f24)) +* **refactor:** refactor openai proxy ([05d0ddd](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/05d0ddd46a0bc06811da77916b21ca7167672149)) +* **reraise:** fix pylint warnings ([d658c38](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/d658c383079791cf6c2455688e4d93c58607de8a)) +* **sonar:** refacor ([14378f3](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/14378f36a1006a97bccd5cf18196c99c0eee9927)) +* **test:** add unit tests for cli ([6b50bf7](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/6b50bf7de0c3977f271c566357ecaf478a07d32a)) +* **test:** fix function call test ([3ad12d0](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/3ad12d095230a5a65db40d5e2acd01999bb35baa)) +* **tests:** add tests ([07e8eec](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/07e8eecb37ff04b777ecd419176f09b4ddb8705d)) +* **unittest:** add langchain unit tests ([#26](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/issues/26)) ([956e714](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/956e71442fd0956c924c7da1851779c24b22b7c8)) +* Licence text ([b60be24](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/b60be246d286a04a343d5605702ad375de766fba)) + +## [1.1.2](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.1...v1.1.2) (2023-11-02) + + +### Bug Fixes + +* **whitesource:** trigger whitesource scan for ECCN ([3a72f38](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/3a72f380dbfc84ce03ba2042966e9223b30f7223)) + +## [1.1.1](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.1.0...v1.1.1) (2023-10-17) + + +### Bug Fixes + +* **readme:** trigger main pipeline for testing cumulus ([8748a09](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/8748a0940284d3e8898a3b9bd211652420ee9e6e)) + +# [1.1.0](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/compare/v1.0.0...v1.1.0) (2023-10-17) + + +### Features + +* **CODEOWNERS:** trigger main build version ([bb42380](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/bb4238000d1eb0f3e9bc3bd3f097a92bb93001c6)) + +# 1.0.0 (2023-10-16) + + +### Bug Fixes + +* **ci:** try sonar ([34d822d](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/34d822de6f01424adc6fd96513018432675a7180)) +* **config:** add missing config ([f1ca6e1](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/f1ca6e196da9f3727160f8f59149c0e408ae4d19)) +* **pylint:** corrections of dockerfile ([0860ec6](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/0860ec6a75e3dd3fad79c5a06f39875a7ff7354c)) +* **unittest:** correct dockerfile for unit tests ([c798f87](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/c798f87768d3ef87fc604cc0a90cd61d38b5889d)) +* **Update Config:** Update configuration ([8195ff2](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/8195ff2f29db2f3aca9ae9788e70bc54ea3e5923)) +* **Update Config:** Update sonar configuration ([fc4a36e](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/fc4a36e7ed9bc8df86cfb77cbb842a4a86c84af7)) +* **whitesource:** fix whitesource user key ([f3447e4](https://github.wdf.sap.corp/AI/ai-core-llm-sdk/commit/f3447e48b322bce13729daaf26b3c2826f60ec14)) diff --git a/packages/gen/docs/Makefile b/packages/gen/docs/Makefile new file mode 100644 index 0000000..b6df959 --- /dev/null +++ b/packages/gen/docs/Makefile @@ -0,0 +1,49 @@ +# Minimal makefile for Sphinx documentation + +# Define the source files and destination directory +FILES := ../README_sphynx.md ../RELEASE_NOTES.md ./gen_ai_hub/examples/gen_ai_hub.ipynb \ + ./gen_ai_hub/examples/orchestration-service.ipynb ./gen_ai_hub/examples/orchestration-service2.ipynb ./gen_ai_hub/examples/streaming.ipynb \ + ./gen_ai_hub/examples/prompt-registry.ipynb ./gen_ai_hub/examples/document-grounding.ipynb ./gen_ai_hub/examples/async-examples.ipynb \ + ./gen_ai_hub/examples/evaluations.ipynb +DESTINATION := ./source/_reference + +# Define HTML +HTML_FILE := ./build/html/index.html + +# Tell make these targets are not files +.PHONY: all check_files copy_files api_doc build comment_out_lines + +# Default target +all: check_files copy_files api_doc build comment_out_lines + +# Check if all files exist +check_files: + @$(foreach file,$(FILES),\ + $(if $(wildcard $(file)),,@echo "File $(file) exists.";,@echo "File $(file) does not exist!" && exit 1;)) + +# Copy files to the destination directory +copy_files: check_files + @mkdir -p $(DESTINATION) + @$(foreach file,$(FILES),\ + cp -v $(file) $(DESTINATION);) + +# Generate API documentation with depth 1, force overwrite and no module index +api_doc: + sphinx-apidoc -o source/_api_doc ../gen_ai_hub -d 1 -T -f -M + +# Build the documentation with Sphinx +build: copy_files + sphinx-build -b html ./source ./build/html + +# Open generated documentation in browser +preview_html: all + @echo "Opening $(HTML_FILE) in the default browser" + @xdg-open $(HTML_FILE) || open $(HTML_FILE) || start $(HTML_FILE) + +# Comment out checkmarx relevant lines +comment_out_lines: + @if [ "$$(uname)" = "Darwin" ]; then \ + sed -i '' '/window\.location/s/^/\/\/ /' ./build/html/_static/doctools.js; \ + else \ + sed -i '/window\.location/s/^/\/\/ /' ./build/html/_static/doctools.js; \ + fi \ No newline at end of file diff --git a/packages/gen/docs/gen_ai_hub.html b/packages/gen/docs/gen_ai_hub.html new file mode 100644 index 0000000..13c14ef --- /dev/null +++ b/packages/gen/docs/gen_ai_hub.html @@ -0,0 +1,20 @@ + + + + +Python: package gen_ai_hub + + + + + +
 
gen_ai_hub
index
(built-in)
+

+

+ + + + + +
 
Package Contents
       
+ \ No newline at end of file diff --git a/packages/gen/docs/gen_ai_hub/README.md b/packages/gen/docs/gen_ai_hub/README.md new file mode 100644 index 0000000..65727d5 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/README.md @@ -0,0 +1,79 @@ +## Configuration for using sdk on SAP AI Core + +To configure and use AI Core proxy module follow the steps below: + +1. Setup an AI Core account in BTP +2. Retrieve the AI Core service key +3. The following parameters are needed: + +- `AICORE_CLIENT_ID`: Client ID +- `AICORE_CLIENT_SECRET`: Client secret +- `AICORE_AUTH_URL`: Url used to retrieve a token using the client id and secret +- `AICORE_BASE_URL`: Url of the service. Use the base url without any additional path +- `AICORE_RESOURCE_GROUP`: Resource group that should be used + +For using X.509 credentials, you can set the file paths to certificate and key files, or certificate and key strings, +as an alternative to client secret. +- `AICORE_CERT_FILE_PATH`: This is the path to the file which holds the X.509 certificate +- `AICORE_KEY_FILE_PATH`: This is the path to the file which holds the X.509 key +- `AICORE_CERT_STR`: This is the content of the X.509 certificate as a string +- `AICORE_KEY_STR`: This is the content of the X.509 key as a string + +The values can be set as environment variables are through config files. For most cases we recommend to used config files. +The config files should be placed in AI Core home folder. Which can be set using the env var `AICORE_HOME`, it is set to +`~/.aicore`, by default. + +To fetch the values from config file instead of setting environment variables, create a config under path `/config.json` +```json + { + "AICORE_AUTH_URL": "https://* * * .authentication.sap.hana.ondemand.com/oauth/token", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CLIENT_SECRET": "* * * ", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +or + +```json + { + "AICORE_AUTH_URL": "https://* * * .authentication.cert.sap.hana.ondemand.com", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CERT_FILE_PATH": "* * */cert.pem", + "AICORE_KEY_FILE_PATH": "* * */key.pem", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +or + +```json + { + "AICORE_AUTH_URL": "https://* * * .authentication.cert.sap.hana.ondemand.com", + "AICORE_CLIENT_ID": "* * * ", + "AICORE_CERT_STR": "* * *", + "AICORE_KEY_STR": "* * *", + "AICORE_RESOURCE_GROUP": "* * * ", + "AICORE_BASE_URL": "https://api.ai.* * *.cfapps.sap.hana.ondemand.com/v2" +} +``` + +You can use the command `aicore configure` to create the needed config file. Use `aicore configure --help` to know various options available. + +See full details of client configuration in the [ai-core-sdk documentation](https://github.wdf.sap.corp/AI/ai-core-sdk/blob/master/PYPIDESCRIPTION.md#client-configuration). + +### Recommended Way: Download key file and use `aicore configure -k ` + +The easiest way to create a config is to download the key file from BTP and +call `aicore configure -k `. + +### Using Multiple Different Profiles + +The default config is expected to be called `config.json`. To use different service keys for different application +one can create separate profiles. The config for a profile has to be called `/config_{profile name}.json`. +The profiles can be selected via the environment variable `AICORE_PROFILE`. For example to create a profile `dox` +one has to create the file `/config_dox.json` and set `AICORE_PROFILE=dox`. + +To create a config file for a profile use `aicore -p configure -k ` Eg. `aicore -p dox configure -k key.json` diff --git a/packages/gen/docs/gen_ai_hub/examples/ai-vs-ai.py b/packages/gen/docs/gen_ai_hub/examples/ai-vs-ai.py new file mode 100644 index 0000000..56eb3ab --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/ai-vs-ai.py @@ -0,0 +1,116 @@ +import uuid +from random import sample +from time import sleep + +from langchain_classic.prompts.chat import ( + ChatPromptTemplate, + MessagesPlaceholder, +) +from langchain_community.chat_message_histories.in_memory import ChatMessageHistory +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.messages import HumanMessage +from langchain_core.runnables.history import RunnableWithMessageHistory + +from gen_ai_hub.proxy.langchain.amazon import ChatBedrock +from gen_ai_hub.proxy.langchain.openai import ChatOpenAI + +store = {} + +available_models = [ + { + "model_class": ChatBedrock, + "model_kwargs": {"model_name": "amazon--titan-text-express"}, + "speaking_name": "Amazon Titan Text Express", + }, + { + "model_class": ChatBedrock, + "model_kwargs": {"model_name": "anthropic--claude-3-haiku"}, + "speaking_name": "Anthropic Claude 3 Haiku", + }, + { + "model_class": ChatOpenAI, + "model_kwargs": {"proxy_model_name": "gpt-4o-mini"}, + "speaking_name": "GPT 4o mini", + }, +] + + +def get_session_history(session_id: str) -> BaseChatMessageHistory: + if session_id not in store: + store[session_id] = ChatMessageHistory() + return store[session_id] + + +def create_session_history() -> str: + session_id = str(uuid.uuid4()) + store[session_id] = ChatMessageHistory() + return session_id + + +def create_instructed_model(base_model, speaking_name, session_id): + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are the large language model {llmname}. Your goal is to answer questions nicely and keep a conversation going. Add a creative question unrelated to the previous conversaiton at the end of your answer. Also do everything with a pirate accent!", + ), + MessagesPlaceholder(variable_name="messages"), + ] + ) + model = prompt | base_model + return { + "model": model, + "speaking_name": speaking_name, + "session_id": session_id, + } + + +def converse_ai(model_dict, prompt) -> str: + config = {"configurable": {"session_id": model_dict["session_id"]}} + model = model_dict["model"] + model_with_message_history = RunnableWithMessageHistory( + model, + get_session_history, + input_messages_key="messages", + ) + response = model_with_message_history.invoke( + { + "messages": [HumanMessage(content=prompt)], + "llmname": model_dict["speaking_name"], + }, + config=config, + ) + return response.content + + +def main(): + session_id_model_1 = create_session_history() + session_id_model_2 = create_session_history() + + model_1, model_2 = sample(available_models, 2) + + model_1 = create_instructed_model( + model_1["model_class"](**model_1["model_kwargs"]), + speaking_name=model_1["speaking_name"], + session_id=session_id_model_1, + ) + model_2 = create_instructed_model( + model_2["model_class"](**model_2["model_kwargs"]), + speaking_name=model_2["speaking_name"], + session_id=session_id_model_2, + ) + + model_of_last_turn = model_1 + model_of_next_turn = model_2 + last_response = "Introduce yourself." + for _ in range(1, 11): + last_response = converse_ai(model_of_next_turn, last_response) + print("#########################################################") + print(model_of_next_turn["speaking_name"] + ": " + last_response) + print("#########################################################") + model_of_next_turn, model_of_last_turn = model_of_last_turn, model_of_next_turn + sleep(3) + + +if __name__ == "__main__": + main() diff --git a/packages/gen/docs/gen_ai_hub/examples/async-examples.ipynb b/packages/gen/docs/gen_ai_hub/examples/async-examples.ipynb new file mode 100644 index 0000000..1317d0b --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/async-examples.ipynb @@ -0,0 +1,534 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "24f8b765-3d67-4d21-aa01-8789cc0f1bc7", + "metadata": {}, + "source": [ + "(async_examples)=\n", + "# Async examples\n", + "\n", + "## Async Amazon native \n", + "This notebook demonstrates how to use async-based calls for Amazon AI models.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c241eeca-d524-4928-94ef-5f38273ac85a", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "\n", + "import json\n", + "from gen_ai_hub.proxy.native.amazon import AsyncSession\n", + "\n", + "# ### Async Function to Invoke Amazon Model\n", + "\n", + "async def async_bedrock_invoke_model():\n", + " session = AsyncSession()\n", + " bedrock = await session.async_client(model_name=\"amazon--nova-premier\")\n", + " body = json.dumps(\n", + " {\n", + " \"inputText\": \"Explain black holes to 8th graders.\",\n", + " \"textGenerationConfig\": {\n", + " \"maxTokenCount\": 300,\n", + " \"stopSequences\": [],\n", + " \"temperature\": 0.0,\n", + " \"topP\": 0.9,\n", + " },\n", + " }\n", + " )\n", + " response = await bedrock.invoke_model(body=body)\n", + " response_body = json.loads(await response.get(\"body\").read())\n", + " print(\"Response:\", response_body)\n", + " await bedrock.close()\n", + "\n", + "\n", + "##%%\n", + "# Run the async functions\n", + "response = await async_bedrock_invoke_model()\n" + ] + }, + { + "cell_type": "markdown", + "id": "2388ad6f-5d72-4755-b4ae-3bb271f2dd67", + "metadata": {}, + "source": [ + "\n", + "## Async Function to Stream Amazon Model Response\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00526fd2-639d-4e0e-8860-e7ae09f0c535", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "async def async_bedrock_invoke_with_stream():\n", + " session = AsyncSession()\n", + " bedrock = await session.async_client(model_name=\"amazon--nova-premier\")\n", + " body = json.dumps(\n", + " {\n", + " \"inputText\": \"You are a story teller. Tell me a short story about boats.\",\n", + " \"textGenerationConfig\": {\n", + " \"maxTokenCount\": 300,\n", + " \"stopSequences\": [],\n", + " \"temperature\": 0.0,\n", + " \"topP\": 0.9,\n", + " },\n", + " }\n", + " )\n", + " async for event in bedrock.invoke_model_with_response_stream(body=body):\n", + " for line in event[\"chunk\"][\"bytes\"].splitlines():\n", + " if line and line.startswith(b\"data: \"):\n", + " line = line[6:]\n", + " chunk = json.loads(line)\n", + " if \"outputText\" in chunk:\n", + " print(\"Chunk Output:\", chunk[\"outputText\"])\n", + "\n", + "# ### Run Async Functions\n", + "await async_bedrock_invoke_with_stream()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "081be38d-6068-408a-97e8-2edc8979d505", + "metadata": {}, + "outputs": [], + "source": [ + "##%% [markdown]\n", + "# ### Async Function to Converse with Amazon Bedrock\n", + "\n", + "##%%\n", + "async def async_amazon_bedrock_converse(model_name):\n", + " session = AsyncSession()\n", + " bedrock = await session.async_client(model_name=model_name)\n", + " conversation = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"text\": \"Describe the purpose of a 'hello world' program in one line.\"\n", + " }\n", + " ],\n", + " }\n", + " ]\n", + "\n", + " response = await bedrock.converse(\n", + " messages=conversation,\n", + " inferenceConfig={\"maxTokens\": 512, \"temperature\": 0.0, \"topP\": 0.9},\n", + " )\n", + " print(\"Response:\", response[\"output\"][\"message\"][\"content\"][0][\"text\"])\n", + " await bedrock.close()\n", + "\n", + "##%% [markdown]\n", + "# ### Run the Async Function\n", + "\n", + "##%%\n", + "# Replace with the desired model name\n", + "await async_amazon_bedrock_converse(\"amazon--nova-premier\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed42d35c-a13d-4fab-94d6-6c434cda96e9", + "metadata": {}, + "source": [ + "\n", + "## Async Function to Test Amazon Titan Embedding\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97c781aa-4b86-4247-9d15-77b22c5d4304", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "async def async_amazon_titan_embedding(model_name):\n", + " session = AsyncSession()\n", + " bedrock = await session.async_client(model_name=model_name)\n", + " body = json.dumps(\n", + " {\n", + " \"inputText\": \"Please recommend books with a theme similar to the movie 'Inception'.\",\n", + " }\n", + " )\n", + " response = await bedrock.invoke_model(body=body)\n", + " response_body = json.loads(await response.get(\"body\").read())\n", + " print(\"Response Metadata:\", response[\"ResponseMetadata\"])\n", + " print(\"Embedding:\", response_body[\"embedding\"])\n", + " await bedrock.close()\n", + "\n", + "##%% [markdown]\n", + "# ### Run the Async Function\n", + "\n", + "##%%\n", + "# Replace with the desired model name\n", + "await async_amazon_titan_embedding(\"amazon--titan-embed-text\")" + ] + }, + { + "cell_type": "markdown", + "id": "f95566c1-5d17-441f-846a-512a354a8161", + "metadata": {}, + "source": [ + "## Async Google Genai native example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d9a8028-868f-417f-81f0-8e281f559765", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "async with Client(proxy_client=proxy_client,).aio as aclient:\n", + " response = await aclient.models.generate_content(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=\"Explain the relativity theory in simple terms.\"\n", + " )\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "1a1dfb2d-f3e8-4d20-bc7a-856fda3f8411", + "metadata": {}, + "source": [ + "## Async Google GenAI Chat Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c95b85c3-f18e-4e1e-8fb6-e175ed8a6da1", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "async with Client(proxy_client=proxy_client).aio as aclient:\n", + " chat_session = aclient.chats.create(\n", + " model=\"gemini-2.5-flash\"\n", + " )\n", + " model_response = await chat_session.send_message(\"Hello.\")\n", + " print(\"Response 1:\", model_response.text)\n", + " \n", + " model_response = await chat_session.send_message(\n", + " \"What is your opinion about latest Gemini model?\"\n", + " )\n", + " print(\"Response 2:\", model_response.text)" + ] + }, + { + "cell_type": "markdown", + "id": "fc70633d-a0e3-4901-b110-f0434b5d843e", + "metadata": {}, + "source": [ + "## Async Google GenAI Stream Generate Content Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d1f0e9d-05a2-47ad-9fe5-406c791960ab", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from google.genai.types import GenerateContentResponse, GenerateContentConfig, Content, Part\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "\n", + "def get_test_messages_for_genAI(text=\"Write a story about a magic backpack.\"):\n", + " if not text:\n", + " text = \"Write a story about a magic backpack.\"\n", + " user_prompt_content = Content(\n", + " role=\"user\",\n", + " parts=[\n", + " Part(text=text),\n", + " ],\n", + " )\n", + " return [user_prompt_content]\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "async with Client(proxy_client=proxy_client).aio as aclient:\n", + " async_response_stream = await aclient.models.generate_content_stream(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=get_test_messages_for_genAI(\n", + " text=\"You are a story teller. Write a paragraph about a magic kingdom.\"\n", + " ),\n", + " config=GenerateContentConfig(temperature=0),\n", + " )\n", + " async for chunk in async_response_stream:\n", + " print(\"Chunk:\", chunk.text)" + ] + }, + { + "cell_type": "markdown", + "id": "aba2e910-460d-4272-b212-4ff05482bc60", + "metadata": {}, + "source": [ + "\n", + "## Langchain examples\n", + "Async Chat Model Example\n", + "This demonstrates how to use the `chat_model.ainvoke` method for the Claude model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dd33ff9-b285-405b-b514-6c9b742a078e", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from langchain_core.messages import HumanMessage, AIMessage\n", + "from gen_ai_hub.proxy.langchain import ChatBedrock\n", + "\n", + "async def async_amazon_chat_model():\n", + " # Initialize the ChatBedrock model with the desired configuration\n", + " chat_model = ChatBedrock(\n", + " model_name=\"anthropic--claude-3-haiku\",\n", + " model_kwargs={\"temperature\": 0.0}\n", + " )\n", + " # Send a message to the model\n", + " response = await chat_model.ainvoke(\n", + " [HumanMessage(content=\"Write me a song about sparkling water.\")]\n", + " )\n", + " # Validate and print the response\n", + " if isinstance(response, AIMessage):\n", + " print(\"Response:\", response.content)\n", + "\n", + "await async_amazon_chat_model()" + ] + }, + { + "cell_type": "markdown", + "id": "e2fe1811-743d-45db-b811-8c430cef5902", + "metadata": {}, + "source": [ + "\n", + "## Async Chat Streaming Example\n", + "This notebook demonstrates how to use the `chat_model.astream` method for the Claude model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4268cc61-cd86-472b-8662-99f23b762213", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from langchain_classic.schema import HumanMessage\n", + "from langchain_core.messages import AIMessageChunk\n", + "from gen_ai_hub.proxy.langchain import ChatBedrock\n", + "\n", + "async def async_chat_streaming():\n", + " # Initialize the ChatBedrock model with streaming enabled\n", + " chat_model = ChatBedrock(\n", + " model_name=\"anthropic--claude-3-haiku\",\n", + " model_kwargs={\"temperature\": 0.0},\n", + " proxy_client=None, # Replace with your proxy client instance\n", + " streaming=True\n", + " )\n", + " chunks = []\n", + " # Stream responses asynchronously\n", + " async for chunk in chat_model.astream([HumanMessage(content=\"Write me a song about sparkling water in 20 words.\")]):\n", + " chunks.append(chunk)\n", + " print(chunk.content) # Print each chunk's content\n", + " # Validate that all chunks are instances of AIMessageChunk\n", + " assert all(isinstance(chunk, AIMessageChunk) for chunk in chunks)\n", + "\n", + "\n", + "await async_chat_streaming()" + ] + }, + { + "cell_type": "markdown", + "id": "2e51cbda-cda7-4776-9f9d-6181f45ab0d9", + "metadata": {}, + "source": [ + "\n", + "## Chat Converse Model Example\n", + "This demonstrates how to use the `ChatBedrockConverse` model's `ainvoke` .\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9826f05f-7ae1-4d4e-aa91-0fed40b93448", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from gen_ai_hub.proxy.langchain import ChatBedrockConverse\n", + "\n", + "async def chat_converse_model_example(model_name):\n", + " try:\n", + " # Initialize the ChatBedrockConverse model\n", + " chat_model = ChatBedrockConverse(\n", + " model_name=model_name,\n", + " model_kwargs={\"temperature\": 0.0}\n", + " )\n", + " # Send a message to the model\n", + " response = await chat_model.ainvoke(\n", + " [HumanMessage(content=\"Write me a song about sparkling water.\")]\n", + " )\n", + " # Check if the response is valid\n", + " if isinstance(response, AIMessage):\n", + " print(\"Response:\", response.content)\n", + " else:\n", + " print(\"Unexpected response type:\", type(response))\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n", + "\n", + "\n", + "await chat_converse_model_example(\"anthropic--claude-3-haiku\")" + ] + }, + { + "cell_type": "markdown", + "id": "b39ec40d-e4de-436b-8e29-9896e5c1a985", + "metadata": {}, + "source": [ + "\n", + "## Async Gemini Model Invocation Example\n", + "This demonstrates how to use the `ainvoke` method of the Gemini model asynchronously.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f60ef8f-020f-42e6-b0e1-6b89e5968bcd", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.langchain import init_llm\n", + "\n", + "async def gemini_ainvoke_example():\n", + " # Initialize the model using init_llm\n", + " llm = init_llm(\n", + " model_name=\"gemini-2.0-flash\",\n", + " max_tokens=300\n", + " )\n", + " # Send a message to the model\n", + " response = await llm.ainvoke(\"Write a ballad about LangChain\")\n", + " print(response)\n", + "\n", + "await gemini_ainvoke_example()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d84e850-053e-47a3-9ce4-e4f7f796b17d", + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-02T13:52:55.224761Z", + "start_time": "2026-01-02T13:52:49.327651Z" + } + }, + "outputs": [], + "source": [ + "from langchain_core.messages import AIMessage\n", + "from gen_ai_hub.proxy.langchain import ChatGoogleGenerativeAI\n", + "\n", + "async def gemini_ainvoke_example():\n", + " # Initialize the ChatGoogleGenerativeAI model\n", + " chat_model = ChatGoogleGenerativeAI(\n", + " proxy_model_name=\"gemini-2.0-flash\",\n", + " max_tokens=300\n", + " )\n", + " # Send a message to the model\n", + " response = await chat_model.ainvoke(\"Write a ballad about LangChain\")\n", + " print(response)\n", + "\n", + "await gemini_ainvoke_example()" + ] + }, + { + "cell_type": "markdown", + "id": "1dab3348-6dd4-4c79-8cd6-783b25660c9a", + "metadata": {}, + "source": [ + "\n", + "## Async Gemini Streaming Example\n", + "This notebook demonstrates how to use the `astream` method of the Gemini chat model asynchronously.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "170481a4-d27c-450a-8e85-5b998e55c5af", + "metadata": { + "ExecuteTime": { + "end_time": "2026-01-02T13:53:48.641725Z", + "start_time": "2026-01-02T13:53:42.710953Z" + } + }, + "outputs": [], + "source": [ + "\n", + "async def gemini_astream_example():\n", + " # Initialize the ChatGoogleGenerativeAI model\n", + " chat_model = ChatGoogleGenerativeAI(\n", + " proxy_model_name=\"gemini-2.0-flash\",\n", + " temperature=0\n", + " )\n", + " # Define the input content\n", + " content = \"You are a storyteller. Write a story about a magic backpack.\"\n", + " # Stream the response\n", + " async for chunk in chat_model.astream(content):\n", + " print(\"Chunk:\", chunk.content)\n", + "\n", + "await gemini_astream_example()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "906f2eca-77be-42f6-a3d2-6673184b2563", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/data/cv.txt b/packages/gen/docs/gen_ai_hub/examples/data/cv.txt new file mode 100644 index 0000000..2dd7a6b --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/data/cv.txt @@ -0,0 +1,42 @@ +Patrick Morgan ++49 30 23125 123 +patric.morgan@example.com + +Highlights + +Strategic and financial planning expert +Accurate forecasting +Process implementation +Staff leadership and development +Business performance improvement +Proficient in SAP, Excel VBA +Education +Master of Science: Finance - 2014 +Harvard University, Boston + +Bachelor of Science: Finance - 2011 +Harvard University, Boston + +Certifications +Certified Management Accountant + +Summary +Skilled Financial Manager adept at increasing work process efficiency and profitability through functional and technical analysis. +Successful at advising large corporations, small businesses, and individual clients. Areas of expertise include asset allocation, investment strategy, and risk management. + +Experience +Finance Manager - 09/2016 to 05/2018 +M&K Group, York + +Manage the modelling, planning, and execution of all financial processes. +Carry short and long-term custom comprehensive financial strategies to reach company goals. +Recommended innovative alternatives to generate revenue and reduce unnecessary costs. +Employed advanced deal analysis, including hands-on negotiations with potential investors. +Research market trends and surveys and use information to stimulate business. +Finance Manager - 09/2013 to 05/2016 +Ago Group, Chicago + +Drafted executive analysis reports highlighting business issues, potential risks, and profit opportunities. +Recommended innovative alternatives to generate revenue and reduce unnecessary costs. +Employed advanced deal analysis, including hands-on negotiations with potential investors. +Analysed market trends and surveys and used information to revenue growth. diff --git a/packages/gen/docs/gen_ai_hub/examples/document-grounding.ipynb b/packages/gen/docs/gen_ai_hub/examples/document-grounding.ipynb new file mode 100644 index 0000000..ec0ab33 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/document-grounding.ipynb @@ -0,0 +1,262 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(document_grounding)=\n", + "# Document Grounding" + ], + "id": "b222f13a3218161a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The Document Grounding is a module in the [Orchestration Service](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/_reference/orchestration-service.html).\n", + "\n", + "The Document Grounding implements the Retrieval Augmented Generation (RAG) approach. It leverages the SAP Hana Vector Engine to retrieve info from relevant documents i.e., the \"context\" and uses them to generate more accurate responses." + ], + "id": "33826c61f2fc4785" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "A vector knowledge base is [required to use the Document Grounding module](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/grounding#prerequisites).\n", + "\n", + "The vector knowledge base can be created from\n", + " - a collection of documents in a [sharepoint folder, S3 storage, or an SFTP repository](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/grounding#option-1-upload-the-documents-to-the-supported-data-repository-and-run-data-pipeline), or\n", + " - feeding text (chunks) directly [via Vector API](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/grounding#option-2-provide-the-chunks-of-document-via-vector-api-directly).\n", + "\n", + "Another option is to use a website which provides elastic search capabilities.\n", + "At the moment, only the help.sap.com is supported." + ], + "id": "ceed52c35c0f35f4" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(document_storage)=\n", + "### Create a Vector knowledge base\n", + "In this example, we will use an S3 data storage, which has been created by the user. The user hase uploaded a set of documents to the S3 bucket.\n", + "\n", + "Check if\n", + " - [Document Grounding is enabled](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-resource-group-for-ai-data-management?q=document%20grounding) and\n", + " - a [Generic Secret for the S3 bucket is created in AI Core](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generic-secrets-for-grounding?q=document%20grounding), which allows for retrieving the documents in the S3 bucket.\n", + "\n", + "The [Pipelines API](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/pipeline-api-a9badce6a4da4df68e98549d64aa2217?q=document%20grounding) can be run via this SDK:\n" + ], + "id": "206d66e3df483d85" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.document_grounding import PipelineAPIClient, S3PipelineCreateRequest, CommonConfiguration\n", + "\n", + "aicore_client = get_proxy_client()\n", + "pipelines_api_client = PipelineAPIClient(aicore_client)\n", + "generic_secret_s3_bucket = \"<*** generic secret name for the S3 bucket ***>\"\n", + "s3_config = S3PipelineCreateRequest(configuration=CommonConfiguration(destination=generic_secret_s3_bucket))\n", + "response = pipelines_api_client.create_pipeline(s3_config)\n", + "print(f\"Reference the Vector knowledge base using the pipeline ID: {response.pipelineId}\")\n", + "# check the status of the vectorization pipeline until it is completed\n", + "print(pipelines_api_client.get_pipeline_status(response.pipelineId))" + ], + "id": "768bf7d996352ba3", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Configuration of the Grounding Module\n", + "Provide the Orchestration Service URL and create a client for the Orchestration Service." + ], + "id": "bc79071c91fcbcc8" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.document_grounding import (GroundingModule, GroundingType, DataRepositoryType,\n", + " GroundingFilterSearch, DocumentGrounding,DocumentGroundingFilter)\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "orchestration_service_url = \"https://api.ai.<*** cluster-name ***>.aws.ml.hana.ondemand.com/v2/inference/deployments/<*** deployment_id ***>\"\n", + "orchestration_service = OrchestrationService(api_url=orchestration_service_url)\n", + "\n", + "llm = LLM(\n", + " name=\"gpt-4o-mini\",\n", + " parameters={\n", + " 'temperature': 0.0,\n", + " }\n", + ")" + ], + "id": "20c108eda849e470", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Create the configuration\n", + "1) Define the prompts\n", + "2) Define the Grounding Module configuration" + ], + "id": "edd772c3a186c8f6" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "\n", + "prompt = Template(messages=[\n", + " SystemMessage(\"You are an expert on SAP Product features.\"),\n", + " UserMessage(\"\"\"Context: {{ ?grounding_response }}\n", + " Question: What are the features of {{ ?product }}\n", + " \"\"\"),\n", + " ])" + ], + "id": "41d6d4f733eef5b9", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "#### Grounding configuration for searching SAP Help via elastic search", + "id": "5b7da7a2a8af2000" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "filters = [DocumentGroundingFilter(id=\"SAPHelp\", data_repository_type=DataRepositoryType.URL.value)]\n", + "\n", + "grounding_config = GroundingModule(type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value,\n", + " config=DocumentGrounding(input_params=[\"product\"],\n", + " output_param=\"grounding_response\",\n", + " filters=filters\n", + " )\n", + " )\n", + "\n", + "config = OrchestrationConfig(template= prompt, llm=llm, grounding=grounding_config)\n", + "\n", + "response = orchestration_service.run(config=config,\n", + " template_values=[TemplateValue(\"product\", \"Generative AI Hub\")])\n", + "\n", + "print(response.orchestration_result.choices[0].message.content)" + ], + "id": "f498206f13e66293", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### Grounding configuration for searching a custom data repository\n", + "Assume the documentation for custom product extension is vectorized and stored in the Vector knowledge base which we created earlier from the S3 bucket." + ], + "id": "86369cedb1ac9eaf" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "filters = [DocumentGroundingFilter(id=\"<*** product extension docs id ***>\",\n", + " data_repositories=[\"<*** data repository (retrieval api) referencing the S3 pipeline id ***>\"],\n", + " search_config=GroundingFilterSearch(max_chunk_count=3),\n", + " data_repository_type=DataRepositoryType.VECTOR.value\n", + " )]\n", + "\n", + "grounding_config = GroundingModule(\n", + " type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value,\n", + " config=DocumentGrounding(input_params=[\"product\"], output_param=\"grounding_response\", filters=filters)\n", + " )\n", + "\n", + "config = OrchestrationConfig(template=prompt, llm=llm, grounding=grounding_config)\n", + "\n", + "response = orchestration_service.run(config=config,\n", + " template_values=[TemplateValue(\"product\", \"<*** custom extension name ***>\")])\n", + "\n", + "print(response.orchestration_result.choices[0].message.content)" + ], + "id": "6eabcada3ecb33d4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "#### One can also show the retrieved context from the grounding module, which is added to the prompt for improving the response.", + "id": "748790f57f6ced8b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "print(response.module_results.grounding.data['grounding_result'])", + "id": "84522d401028dff8", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(data-masking)=\n", + "\n", + "#### Data Masking of the retrieved context\n", + "The retrieved context can be masked in the same way as in the Orchestration Service to avoid passing sensitive information to the LLM." + ], + "id": "e400b66c8da64d0b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration.models.sap_data_privacy_integration import SAPDataPrivacyIntegration, MaskingMethod, ProfileEntity\n", + "from gen_ai_hub.orchestration.models.data_masking import DataMasking\n", + "\n", + "data_masking = DataMasking(\n", + " providers=[\n", + " SAPDataPrivacyIntegration(\n", + " method=MaskingMethod.ANONYMIZATION,\n", + " entities=[ProfileEntity.SAP_IDS_INTERNAL],\n", + " mask_grounding_input=True\n", + " )\n", + " ]\n", + ")\n", + "masking_config = OrchestrationConfig(template=prompt, llm=llm, grounding=grounding_config, data_masking=data_masking)\n", + "response = orchestration_service.run(config=masking_config,\n", + " template_values=[TemplateValue(\"product\", \"<*** custom extension name ***>\")])\n", + "\n", + "print(response.orchestration_result.choices[0].message.content)\n", + "\n", + "print(response.module_results.grounding.data['grounding_result'])" + ], + "id": "ff5aa7b42615794c", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/evaluations.ipynb b/packages/gen/docs/gen_ai_hub/examples/evaluations.ipynb new file mode 100755 index 0000000..6452377 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/evaluations.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(evaluations)=\n", + "# Generative AI Custom Evaluation\n", + "This is an example notebook which showcases how a user can use Evaluations SDK to benchmark their large language models, evaluate orchestration configuration or prompts for their use case.\n", + "It uses publicly available [MedicationQA dataset](https://langtest.org/docs/pages/benchmarks/medical/medicationqa/) which consists of commonly asked consumer questions about medications. The workload computes industry standard metrics to check the reliability of the response generate by llm." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Loading the credentials from the env file\n", + "from gen_ai_hub.evaluations import EvaluationClient\n", + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "load_dotenv(override=True)\n", + "\n", + "AICORE_BASE_URL = os.getenv(\"AICORE_BASE_URL\")\n", + "AICORE_RESOURCE_GROUP = os.getenv(\"AICORE_RESOURCE_GROUP\")\n", + "AICORE_AUTH_URL = os.getenv(\"AICORE_AUTH_URL\")\n", + "AICORE_CLIENT_ID = os.getenv(\"AICORE_CLIENT_ID\")\n", + "AICORE_CLIENT_SECRET = os.getenv(\"AICORE_CLIENT_SECRET\")\n", + "\n", + "AWS_ACCESS_KEY_ID = os.getenv(\"AWS_ACCESS_KEY_ID\")\n", + "AWS_BUCKET_ID = os.getenv(\"AWS_BUCKET_ID\")\n", + "AWS_REGION = os.getenv(\"AWS_REGION\")\n", + "AWS_SECRET_ACCESS_KEY = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n", + "ORCHESTRATION_URL = os.getenv(\"ORCHESTRATION_URL\")\n", + "\n", + "\n", + "client = EvaluationClient(\n", + " # direct ai_core_client can be added as a parameter if already created\n", + " base_url=AICORE_BASE_URL,\n", + " auth_url=AICORE_AUTH_URL,\n", + " client_id=AICORE_CLIENT_ID,\n", + " client_secret=AICORE_CLIENT_SECRET,\n", + " resource_group=AICORE_RESOURCE_GROUP,\n", + " aws_access_key_id=AWS_ACCESS_KEY_ID,\n", + " aws_secret_access_key=AWS_SECRET_ACCESS_KEY,\n", + " orchestration_url=ORCHESTRATION_URL\n", + " )\n", + "\n", + "# One more way to initialize the client\n", + "# client = EvaluationClient.from_env()\n", + "\n", + "print(client.base_url)\n", + "print(client.ai_core_client.object_store_secrets.query(top=10,resource_group=AICORE_RESOURCE_GROUP))\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: If the user is using the default resource group, an orchestration URL is already available and will be selected automatically. If the user has previously created object store secrets, this setup step can be skipped." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## One Time Creation of Secrets" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "\n", + "AWS_S3_ENDPOINT = \"s3-eu-central-1.amazonaws.com\"\n", + "\n", + "# default secret is needed to store output artifacts that the evaluation job creates after it is completed\n", + "default_secret_creds = {\n", + " \"data\": {},\n", + " \"type\": \"S3\",\n", + " \"pathPrefix\": \"sdkOutputFiles\",\n", + " \"endpoint\": AWS_S3_ENDPOINT,\n", + " \"bucket\": AWS_BUCKET_ID,\n", + " \"region\": AWS_REGION,\n", + " \"usehttps\": \"1\",\n", + "}\n", + "\n", + "# input secret is used to load input artifacts required by the evaluation job. \n", + "# This is optional as these files can be loaded via default secret path as well.\n", + "input_secret_creds = {\n", + " \"data\": {},\n", + " \"name\": \"sdk-data\",\n", + " \"type\": \"S3\",\n", + " \"pathPrefix\": \"sdk_input_files/data\",\n", + " \"endpoint\": AWS_S3_ENDPOINT,\n", + " \"bucket\": AWS_BUCKET_ID,\n", + " \"region\": AWS_REGION,\n", + " \"usehttps\": \"1\",\n", + "}\n", + "\n", + "# Function Scope:\n", + "# Creation of object store secrets and creates orchestration deployment url if not passed via initialization.\n", + "response = client.setup(\n", + " default_secret_body=default_secret_creds, input_secret_body=input_secret_creds, replace_existing=True\n", + ")\n", + "\n", + "print(json.dumps(response, indent=4, ensure_ascii=False))\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper function to list available models in the region" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\n", + "models_list = client.list_available_models()\n", + "model_names = [m[\"model\"] for m in models_list]\n", + "print(model_names)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper function to see available System Defined Metrics" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "metrics_list = client.get_system_supported_metrics() # Fetches metrics info from Metric Management Service.\n", + "metric_names = [m[\"name\"] for m in metrics_list]\n", + "print(metric_names)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining the Evaluation Config\n", + "To know more about the structure and type of the parameters in any of the functions, one can just do help on it and see the docstring. For example this is for EvaluationConfig" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from gen_ai_hub.evaluations import EvaluationConfig\n", + "print(help(EvaluationConfig))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper function to generate Evaluation Config\n", + "There are multiple ways to create EvaluationConfig. With prompt template (inline or reference) or orchestration registry reference, with multiple metric configurations in each EvaluationConfig." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from gen_ai_hub.prompt_registry import (\n", + " PromptTemplateSpec,\n", + " PromptTemplate,\n", + " )\n", + "from gen_ai_hub.orchestration.models.template_ref import TemplateRef\n", + "from gen_ai_hub.evaluations import Dataset, MetricConfig, MetricRef, EvaluationConfig\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "\n", + "evaluation_config_list = [\n", + " EvaluationConfig(\n", + " llm=LLM(name=\"gpt-4o\", version=\"latest\"),\n", + " template=PromptTemplateSpec(\n", + " template=[\n", + " PromptTemplate(\n", + " role=\"user\", \n", + " content=\"Provide a concise and informative response to the following consumer health question: {{?question}}\"\n", + " )\n", + " ]\n", + " ), \n", + " template_variable_mapping={\"question\": \"topic\"},\n", + " dataset_config=Dataset(\"eval-data/testdata/medicalqna_dataset.csv\"),\n", + " metrics=[\n", + " MetricConfig(\n", + " reference=MetricRef(id=\"3ea07c1f-5b10-4b12-bf46-6d429faf8010\"),\n", + " variable_mapping={\"reference\": \"ground_truth\"},\n", + " ),\n", + " ],\n", + " ),\n", + " EvaluationConfig(\n", + " orchestration_registry_reference=\"fa938934-ca94-4f8d-b59d-76c4570f0394\",\n", + " template_variable_mapping={\"question\": \"topic\"},\n", + " dataset_config=Dataset(\"eval-data/testdata/medicalqna_dataset.csv\"),\n", + " metrics=[\n", + " MetricConfig(\n", + " reference=MetricRef(id=\"3ea07c1f-5b10-4b12-bf46-6d429faf8010\"),\n", + " variable_mapping={\"reference\": \"ground_truth\"},\n", + " ),\n", + " MetricConfig(\n", + " reference=MetricRef(name=\"Content Filter on Input\"),\n", + " ), \n", + " ],\n", + " ),\n", + " EvaluationConfig(\n", + " llm=LLM(name=\"gpt-5\", version=\"latest\"),\n", + " template=TemplateRef(id=\"73282020-9141-46af-981f-c4816dd01d33\"),\n", + " template_variable_mapping={\"question\": \"topic\"},\n", + " dataset_config=Dataset(\"eval-data/testdata/medicalqna_dataset.csv\"),\n", + " metrics=[\n", + " MetricConfig(\n", + " reference=MetricRef(\n", + " name=\"Pointwise Instruction Following\",\n", + " ),\n", + " ),\n", + " ],\n", + " ),\n", + " ]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running the evaluate function:\n" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "evaluation_runs = client.evaluate(evaluation_config_list)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for current_run in evaluation_runs:\n", + " print(\"values for current run id are ******************** \")\n", + " for key, value in vars(current_run).items():\n", + " if key == \"id\" or key == \"status\":\n", + " print(f\"{key}: {value}\")\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Wait till each of the run is completed:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "for current_run in evaluation_runs:\n", + " print(\"waiting for the current run id of \", current_run.id)\n", + " current_run.wait_for_completion(timeout=3600) # default timeout of 20mins can be overridden by providing a timeout paranmeter in seconds. So in this case it would be 60 min" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "current_run_0 = evaluation_runs[0]\n", + "current_run_1 = evaluation_runs[1]\n", + "current_run_2= evaluation_runs[2]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Debugging\n", + "To debug the evaluation job in case of failures, can use these helper method on run object to get more details" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "\n", + "debug_info = current_run.get_debug_info() # To get the debug information related to status\n", + "print(\"debug info is \", debug_info)\n", + "\n", + "debug_logs = current_run.get_debug_logs() # To see the full trace of logs of the evaluation job\n", + "print(\"Logs of evaluation job are \", json.dumps(debug_logs,indent=4,default=str))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Viewing the Aggregate Results" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "run1_data = current_run_0.results()\n", + "run2_data = current_run_1.results().aggregations()\n", + "run3_data = current_run_2.results().aggregations()\n", + "\n", + "print(run1_data) # The aggregations results are fetched from ML Tracking Service\n", + "# To get the aggregation results for other runs replace run1_data with other vars" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Viewing the Completion Response" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\n", + "data = current_run.results().completions()\n", + "\n", + "def wrap_column(df, col, col_px=200):\n", + " df = df.drop(columns=[\"created_at\", \"updated_at\"])\n", + " return (\n", + " df.style.set_table_styles([\n", + " {\"selector\": \"table\", \"props\": \"table-layout: fixed; width: 100%;\"},\n", + " {\"selector\": f\"td.col{df.columns.get_loc(col)}\",\n", + " \"props\": f\"max-width: {col_px}px; white-space: pre-wrap; word-break: break-word;\"}\n", + " ], overwrite=False)\n", + " )\n", + "\n", + "wrap_column(data.head(), \"completion_result\", col_px=700)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Viewing the Metric Evaluation response" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "data = current_run.results().metrics()\n", + "\n", + "def wrap_column(df, col, col_px=200):\n", + " df = df.drop(columns=[\"created_at\", \"updated_at\"])\n", + " return (\n", + " df.style.set_table_styles([\n", + " {\"selector\": \"table\", \"props\": \"table-layout: fixed; width: 100%;\"},\n", + " {\"selector\": f\"td.col{df.columns.get_loc(col)}\",\n", + " \"props\": f\"max-width: {col_px}px; white-space: pre-wrap; word-break: break-word;\"}\n", + " ], overwrite=False)\n", + " )\n", + "\n", + "wrap_column(data.head(), \"metric_result\", col_px=700)" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/gen_ai_hub.ipynb b/packages/gen/docs/gen_ai_hub/examples/gen_ai_hub.ipynb new file mode 100644 index 0000000..458f7bb --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/gen_ai_hub.ipynb @@ -0,0 +1,984 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5356ac85dd313c4a", + "metadata": {}, + "source": [ + "(gen_ai_hub)=\n", + "Our SDK offers a developer-friendly way to consume foundational models available in the SAP generative AI hub. We strive to facilitate seamless interactions with these models by providing integrations that act as drop-in replacements for the native client SDKs and LangChain. This allows developers to use familiar interfaces and workflows.\n", + "Usage is as follows." + ] + }, + { + "cell_type": "markdown", + "id": "7150b4eea5c2c0b", + "metadata": {}, + "source": [ + "# Native Client Integrations" + ] + }, + { + "metadata": {}, + "cell_type": "raw", + "source": [ + "As of now, there are integrations with three types of native client SDKs (OpenAI, Google, Amazon).\n", + "\n", + "The following contains at least one example per SDK. Note: Some providers share the same interface and can be consumed using the same api. For example, Anthropic Claude and Amazon Nova can be used with the Amazon api.\n", + "\n", + "The list of the available models can be found here: [](supported_models)" + ], + "id": "56352a2c6587b70e" + }, + { + "cell_type": "markdown", + "id": "291111a616be90ce", + "metadata": {}, + "source": [ + "## Completions" + ] + }, + { + "cell_type": "markdown", + "id": "7c64409e5cedfa54", + "metadata": {}, + "source": [ + "### OpenAI" + ] + }, + { + "cell_type": "markdown", + "id": "622cae5091a48f03", + "metadata": {}, + "source": [ + "`Completions` equivalent to `openai.Completions`.\n", + "Below is an example usage of Completions in generative AI hub sdk.\n", + "All models that support the legacy completion endpoint can be used." + ] + }, + { + "cell_type": "code", + "id": "f3361412ae0479ce", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.openai import completions\n", + "\n", + "response = completions.create(\n", + " model_name=\"gpt-4o-mini\",\n", + " prompt=\"The Answer to the Ultimate Question of Life, the Universe, and Everything is\",\n", + " max_tokens=20,\n", + " temperature=0\n", + ")\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "7258e7728a682d05", + "metadata": {}, + "source": [ + "`ChatCompletions` equivalent to `openai.ChatCompletions`\n", + "Below is an example usage of ChatCompletions in generative AI hub sdk." + ] + }, + { + "cell_type": "code", + "id": "c48708a4a05d33bb", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.openai import chat\n", + "\n", + "messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": \"Does Azure OpenAI support customer managed keys?\"},\n", + " {\"role\": \"assistant\", \"content\": \"Yes, customer managed keys are supported by Azure OpenAI.\"},\n", + " {\"role\": \"user\", \"content\": \"Do other Azure Cognitive Services support this too?\"}]\n", + "\n", + "kwargs = dict(model_name='gpt-4o-mini', messages=messages)\n", + "response = chat.completions.create(**kwargs)\n", + "\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "ddb67637b98c53b5", + "metadata": {}, + "source": [ + "#example where model_name is passed with model_version parameter\n", + "\n", + "from gen_ai_hub.proxy.native.openai import chat\n", + "\n", + "messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": \"Does Azure OpenAI support customer managed keys?\"},\n", + " {\"role\": \"assistant\", \"content\": \"Yes, customer managed keys are supported by Azure OpenAI.\"},\n", + " {\"role\": \"user\", \"content\": \"Do other Azure Cognitive Services support this too?\"}]\n", + "\n", + "response = chat.completions.create(model_name='gpt-4o-mini', model_version=\"latest\", messages=messages)\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "#example where deployment_id is passed instead of model_name parameter\n", + "\n", + "from gen_ai_hub.proxy.native.openai import chat\n", + "\n", + "messages = [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": \"Does Azure OpenAI support customer managed keys?\"},\n", + " {\"role\": \"assistant\", \"content\": \"Yes, customer managed keys are supported by Azure OpenAI.\"},\n", + " {\"role\": \"user\", \"content\": \"Do other Azure Cognitive Services support this too?\"}]\n", + "\n", + "response = chat.completions.create(deployment_id=\"dcef02e219ae4916\", messages=messages)\n", + "print(response)" + ], + "id": "73277283655f9e8a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(responses_api)=\n", + "#### Responses API\n", + "\n", + "`Responses` equivalent to `openai.Responses`.\n", + "Below is an example usage of Responses in generative AI hub sdk.\n", + "\n", + "see https://developers.openai.com/api/docs/guides/migrate-to-responses" + ], + "id": "ab5d20c9c0316f09" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.proxy.native.openai import responses\n", + "\n", + "response = responses.create(\n", + " model=\"gpt-5\",\n", + " instructions=\"You are a helpful assistant.\",\n", + " input=\"What is the capital of France?\",\n", + ")\n", + "print(response.output_text)" + ], + "id": "3b0dd159b95cecf1", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "eb8ac14b5f48222d", + "metadata": {}, + "source": [ + "#### Structured model outputs\n", + "LLM output as json objects is a powerful feature that allows you to define the structure of the output you expect from the model.\n", + "\n", + "see https://platform.openai.com/docs/guides/structured-outputs/examples" + ] + }, + { + "cell_type": "code", + "id": "15c782df8d446d3a", + "metadata": {}, + "source": [ + "from pydantic import BaseModel\n", + "from gen_ai_hub.proxy.native.openai import chat, responses\n", + "\n", + "class Person(BaseModel):\n", + " name: str\n", + " age: int\n", + "\n", + "response = chat.completions.parse(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[{\"role\": \"user\", \"content\": \"Tell me about John Doe, aged 30.\"}],\n", + " response_format=Person\n", + ")\n", + "person = response.choices[0].message.parsed # Fully typed Person\n", + "print(person)\n", + "\n", + "# Example using responses\n", + "\n", + "response = responses.parse(\n", + " model=\"gpt-5\",\n", + " input=\"Tell me about John Doe aged 30.\",\n", + " text_format=Person\n", + ")\n", + "print(response.output_parsed) # Fully typed Person" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "fae3e1b8ce0d8b25", + "metadata": {}, + "source": [ + "### Google GenAI" + ] + }, + { + "cell_type": "markdown", + "id": "9017f5382113d612", + "metadata": {}, + "source": [ + "Generate Content" + ] + }, + { + "cell_type": "code", + "id": "a433664b7e7cdc", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "client = Client(proxy_client=proxy_client)\n", + "\n", + "response = client.models.generate_content(model=\"gemini-2.5-flash\",\n", + " contents=\"How many paws are there for a dog?\"\n", + ")\n", + "\n", + "print(response)\n", + "# Using another model\n", + "response = client.models.generate_content(model=\"gemini-2.0-flash\",\n", + " contents=\"Explain the theory of relativity in simple terms.\")\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "a71173b99c2d9899", + "metadata": {}, + "source": [ + "Generate Content streaming" + ] + }, + { + "cell_type": "code", + "id": "147a33eb04a093c0", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "\n", + "client = Client(\n", + " proxy_client=proxy_client,\n", + ")\n", + "\n", + "\n", + "response_stream = client.models.generate_content_stream(model=\"gemini-2.5-flash\",\n", + "contents=\"Explain singularity in short terms.\")\n", + "\n", + "for chunk in response_stream:\n", + " print(\"Chunk: \", chunk.text)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ad3b3835075058d6", + "metadata": {}, + "source": [ + "Functional Calling of Google Genai" + ] + }, + { + "cell_type": "code", + "id": "eb1aa581bf7a45cf", + "metadata": {}, + "source": [ + "from google.genai import types\n", + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "def get_current_weather(location: str) -> str:\n", + " \"\"\"Returns the current weather.\n", + "\n", + " Args:\n", + " location: The city and state, e.g. San Francisco, CA\n", + " \"\"\"\n", + " return 'sunny'\n", + "\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "\n", + "client = Client(\n", + " proxy_client=proxy_client,\n", + ")\n", + "response = client.models.generate_content(\n", + " model='gemini-2.5-flash',\n", + " contents='What is the weather like in Boston?',\n", + " config=types.GenerateContentConfig(tools=[get_current_weather]),\n", + ")\n", + "response" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "f49c89276fd53a58", + "metadata": {}, + "source": [ + "### Amazon" + ] + }, + { + "cell_type": "markdown", + "id": "be6c7e47f6556acc", + "metadata": {}, + "source": [ + "Invoke Model" + ] + }, + { + "cell_type": "code", + "id": "78fcab8a1acdf8bb", + "metadata": {}, + "source": [ + "import json\n", + "from gen_ai_hub.proxy.native.amazon import Session\n", + "\n", + "bedrock = Session().client(model_name=\"amazon--nova-premier\")\n", + "body = json.dumps(\n", + " {\n", + " \"inputText\": \"Explain black holes in astrophysics to 8th graders.\",\n", + " \"textGenerationConfig\": {\n", + " \"maxTokenCount\": 3072,\n", + " \"stopSequences\": [],\n", + " \"temperature\": 0.7,\n", + " \"topP\": 0.9,\n", + " },\n", + " }\n", + ")\n", + "response = bedrock.invoke_model(body=body)\n", + "response_body = json.loads(response.get(\"body\").read())\n", + "print(response_body)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "5339798db523e5df", + "metadata": {}, + "source": [ + "Converse" + ] + }, + { + "cell_type": "code", + "id": "2a9e5a9cbde6328c", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.amazon import Session\n", + "\n", + "bedrock = Session().client(model_name=\"anthropic--claude-4-sonnet\")\n", + "conversation = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"text\": \"Describe the purpose of a 'hello world' program in one line.\"\n", + " }\n", + " ],\n", + " }\n", + "]\n", + "response = bedrock.converse(\n", + " messages=conversation,\n", + " inferenceConfig={\"maxTokens\": 512, \"temperature\": 0.5, \"topP\": 0.9},\n", + ")\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "63b2d5846170e9bb", + "metadata": {}, + "source": [ + "## Embeddings" + ] + }, + { + "cell_type": "markdown", + "id": "ca8885b5011ab4fb", + "metadata": {}, + "source": [ + "### OpenAI" + ] + }, + { + "cell_type": "markdown", + "id": "9a3998e2398fea0b", + "metadata": {}, + "source": [ + "`Embeddings` are equivalent to `openai.Embeddings`. See below examples of how to use `Embeddings` in generative AI hub sdk." + ] + }, + { + "cell_type": "code", + "id": "a69126ffa4e667d0", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.openai import embeddings\n", + "\n", + "response = embeddings.create(\n", + " input=\"Every decoding is another encoding.\",\n", + " model_name=\"text-embedding-ada-002\"\n", + ")\n", + "print(response.data)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "1e4d8abd6de01ef2", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.openai import embeddings\n", + "# example with encoding format passed as parameter\n", + "response = embeddings.create(\n", + " input=\"Every decoding is another encoding.\",\n", + " model_name=\"text-embedding-ada-002\",\n", + " encoding_format='base64'\n", + ")\n", + "print(response.data)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "b587163059751de6", + "metadata": {}, + "source": [ + "### Amazon" + ] + }, + { + "cell_type": "code", + "id": "9f92b5579e675194", + "metadata": {}, + "source": [ + "import json\n", + "from gen_ai_hub.proxy.native.amazon import Session\n", + "bedrock = Session().client(model_name=\"amazon--nova-premier\")\n", + "body = json.dumps(\n", + " {\n", + " \"inputText\": \"Please recommend books with a theme similar to the movie 'Inception'.\",\n", + " }\n", + ")\n", + "response = bedrock.invoke_model(\n", + " body=body,\n", + ")\n", + "response_body = json.loads(response.get(\"body\").read())\n", + "print(response_body)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "ee16f8afd84ab9ec", + "metadata": {}, + "source": [], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "a653390de3119ddf", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.native.openai import embeddings\n", + "# example with encoding format passed as parameter\n", + "response = embeddings.create(\n", + " input=\"Every decoding is another encoding.\",\n", + " model_name=\"text-embedding-ada-002\",\n", + " encoding_format='base64'\n", + ")\n", + "print(response.data)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "8b0ca6d4ec71f8c4", + "metadata": {}, + "source": [ + "# Langchain Integration" + ] + }, + { + "cell_type": "markdown", + "id": "9455e84a86c6adf3", + "metadata": {}, + "source": [ + "LangChain provides an interface that abstracts provider-specific details into a common interface. Classes like Chat and Embeddings are interchangeable.\n", + "\n", + "The list of the available models can be found here: [](supported_models)" + ] + }, + { + "cell_type": "markdown", + "id": "32c93eca0fc4ae60", + "metadata": {}, + "source": [ + "## Harmonized Model Initialization\n", + "The `init_llm` and `init_embedding_model` functions allow easy initialization of langchain model interfaces in a harmonized way in generative AI hub sdk" + ] + }, + { + "cell_type": "code", + "id": "63312fa4e955b44b", + "metadata": {}, + "source": [ + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from gen_ai_hub.proxy.langchain import init_llm\n", + "\n", + "template = \"\"\"Question: {question}\n", + " Answer: Let's think step by step.\"\"\"\n", + "prompt = PromptTemplate(template=template, input_variables=['question'])\n", + "question = 'What is a supernova?'\n", + "\n", + "llm = init_llm('gpt-5-nano', max_tokens=300)\n", + "chain = prompt | llm | StrOutputParser()\n", + "response = chain.invoke({'question': question})\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "1567181d179fab0e", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.langchain import init_embedding_model\n", + "\n", + "text = 'Every decoding is another encoding.'\n", + "\n", + "embeddings = init_embedding_model('text-embedding-ada-002')\n", + "response = embeddings.embed_query(text)\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "19f8e2a18402e747", + "metadata": {}, + "source": [ + "## LLM\n" + ] + }, + { + "cell_type": "code", + "id": "2af160b0ad2ee60d", + "metadata": {}, + "source": [ + "from langchain import PromptTemplate\n", + "\n", + "from gen_ai_hub.proxy.langchain import OpenAI # langchain class representing the AICore OpenAI models\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "# non-chat model\n", + "model_name = \"mistralai--mistral-small-instruct\"\n", + "\n", + "llm = OpenAI(proxy_model_name=model_name, proxy_client=proxy_client) # can be used as usual with langchain\n", + "\n", + "template = \"\"\"Question: {question}\n", + "\n", + "Answer: Let's think step by step.\"\"\"\n", + "\n", + "prompt = PromptTemplate(template=template, input_variables=[\"question\"])\n", + "llm_chain = prompt | llm\n", + "\n", + "question = \"What NFL team won the Super Bowl in the year Justin Bieber was born?\"\n", + "\n", + "print(llm_chain.invoke({'question': question}))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ab679928d97b1e87", + "metadata": {}, + "source": [ + "## Chat model" + ] + }, + { + "cell_type": "code", + "id": "e9cdb2eee2fe33f4", + "metadata": {}, + "source": [ + "from langchain.prompts.chat import (\n", + " AIMessagePromptTemplate,\n", + " ChatPromptTemplate,\n", + " HumanMessagePromptTemplate,\n", + " SystemMessagePromptTemplate,\n", + ")\n", + "\n", + "from gen_ai_hub.proxy.langchain import ChatOpenAI\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "\n", + "chat_llm = ChatOpenAI(proxy_model_name='gpt-4o-mini', proxy_client=proxy_client)\n", + "template = 'You are a helpful assistant that translates english to pirate.'\n", + "\n", + "system_message_prompt = SystemMessagePromptTemplate.from_template(template)\n", + "\n", + "example_human = HumanMessagePromptTemplate.from_template('Hi')\n", + "example_ai = AIMessagePromptTemplate.from_template('Ahoy!')\n", + "human_template = '{text}'\n", + "\n", + "human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)\n", + "chat_prompt = ChatPromptTemplate.from_messages(\n", + " [system_message_prompt, example_human, example_ai, human_message_prompt])\n", + "\n", + "chain = chat_prompt | chat_llm\n", + "\n", + "response = chain.invoke({'text': 'I love planking.'})\n", + "print(response.content)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "45a1491cbb71ddb3", + "metadata": {}, + "source": [ + "### Structured model outputs" + ] + }, + { + "cell_type": "code", + "id": "28db422fdb96cff9", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.langchain import ChatOpenAI\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "from langchain.schema import HumanMessage\n", + "from pydantic import BaseModel\n", + "\n", + "class Person(BaseModel):\n", + " name: str\n", + " age: int\n", + "chat_model = ChatOpenAI(proxy_model_name=\"gpt-4o-mini\", proxy_client=get_proxy_client())\n", + "chat_model = chat_model.with_structured_output(method=\"json_schema\", schema=Person, strict=True)\n", + "\n", + "message = HumanMessage(content=\"Tell me about a person named John who is 30\")\n", + "print(chat_model.invoke([message]))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ba68b11bd38cf1b1", + "metadata": {}, + "source": [ + "## Embeddings" + ] + }, + { + "cell_type": "code", + "id": "e000701b370d9f1f", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.langchain import OpenAIEmbeddings\n", + "from gen_ai_hub.proxy import get_proxy_client\n", + "\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "\n", + "embedding_model = OpenAIEmbeddings(proxy_model_name='text-embedding-ada-002', proxy_client=proxy_client)\n", + "\n", + "response = embedding_model.embed_query('Every decoding is another encoding.')\n", + "\n", + "#call without passing proxy_client\n", + "\n", + "embedding_model = OpenAIEmbeddings(proxy_model_name='text-embedding-ada-002')\n", + "\n", + "response = embedding_model.embed_query('Every decoding is another encoding.')\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "a6e8c10a347923c6", + "metadata": {}, + "source": [ + "(rpt_models)=\n", + "# SAP RPT-1 Models\n", + "\n", + "SAP-RPT-1 is a relational pretrained transformer for use on relational and structured data. It's developed and maintained by SAP.\n", + "\n", + "Relational Foundation Models (RFMs) are large-scale machine learning models designed to understand, process, and do predictions on tabular and relational data.\n", + "\n", + "SAP-RPT-1 is a table-native model aiming to achieve highest prediction quality and lowest error rates for predictions on tabular business data. It's pretrained, and doesn't need additional training or fine-tuning steps.\n", + "\n", + "RPT-1 solves predictive tasks such as classification and regression out-of-the-box without requiring any training or fine-tuning via in-context learning. Due to its table-native architecture, prediction quality on enterprise data is typically very high, ahead of state-of-the-art narrow AI models and LLMs employed for such tasks.\n", + "\n", + "You can get predictions directly by the native SDK client.\n", + "\n", + "For detailed information about RPT model API visit this [page](https://help.sap.com/docs/sap-ai-core/generative-ai/example-payloads-for-inferencing-sap-rpt-1?locale=en-US)" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Example of usage RPTClient for the regression task\n", + "\n", + "This is simple example of `RPTClient` usage with minimall fields in body and with pydantic models usage." + ], + "id": "2e3700dfa4ca1ed8" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.proxy.native.sap import RPTRequest, PredictionConfig, TargetColumn, RPTClient\n", + "\n", + "rows_regression = [\n", + " {\n", + " \"PRODUCT\": \"Couch\",\n", + " \"PRICE\": 999.99,\n", + " \"ORDERDATE\": \"28-11-2025\",\n", + " \"ID\": \"35\",\n", + " \"DISCOUNT_RATE\": \"[PREDICT]\",\n", + " },\n", + " {\n", + " \"PRODUCT\": \"Office Chair\",\n", + " \"PRICE\": 150.80,\n", + " \"ORDERDATE\": \"02-11-2025\",\n", + " \"ID\": \"44\",\n", + " \"DISCOUNT_RATE\": 0.12,\n", + " },\n", + " {\n", + " \"PRODUCT\": \"Server Rack\",\n", + " \"PRICE\": 2200.00,\n", + " \"ORDERDATE\": \"01-11-2025\",\n", + " \"ID\": \"104\",\n", + " \"DISCOUNT_RATE\": 0.05,\n", + " },\n", + " {\n", + " \"PRODUCT\": \"Standing Desk\",\n", + " \"PRICE\": 640.00,\n", + " \"ORDERDATE\": \"05-11-2025\",\n", + " \"ID\": \"205\",\n", + " \"DISCOUNT_RATE\": 0.10,\n", + " },\n", + " {\n", + " \"PRODUCT\": \"Monitor 27 inch\",\n", + " \"PRICE\": 289.99,\n", + " \"ORDERDATE\": \"08-11-2025\",\n", + " \"ID\": \"306\",\n", + " \"DISCOUNT_RATE\": \"[PREDICT]\",\n", + " },\n", + "]\n", + "client = RPTClient()\n", + "body = RPTRequest(\n", + " prediction_config=PredictionConfig(\n", + " target_columns=[\n", + " TargetColumn(name=\"DISCOUNT_RATE\", task_type=\"regression\")\n", + " ]),\n", + " rows=rows_regression\n", + " )\n", + "response = client.predict(body=body, model_name=\"sap-rpt-1-small\")\n", + "print(response.predictions)\n", + "\n", + "#example with model_version\n", + "response = client.predict(body=body, model_name=\"sap-rpt-1-small\", model_version=\"latest\")\n", + "print(response.predictions)" + ], + "id": "464856504d307aa8", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Example of usage RPTClient for the classification task\n", + "\n", + "This example shows the possibility to use just dictionary for `RPTClient`." + ], + "id": "29f1b61cce92ab63" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "example_request_by_columns_dict = {\n", + " \"prediction_config\": {\n", + " \"target_columns\": [\n", + " {\n", + " \"name\": \"COSTCENTER\",\n", + " \"prediction_placeholder\": \"[PREDICT]\",\n", + " \"task_type\": \"classification\"\n", + " }\n", + " ]\n", + " },\n", + " \"columns\": {\n", + " \"PRODUCT\": [\"Couch\", \"Office Chair\", \"Server Rack\"],\n", + " \"PRICE\": [999.99, 150.8, 2200.00],\n", + " \"ORDERDATE\": [\"28-11-2025\", \"02-11-2025\", \"01-11-2025\"],\n", + " \"ID\": [\"35\", \"44\", \"104\"],\n", + " \"COSTCENTER\": [\"[PREDICT]\", \"Office Furniture\", \"Data Infrastructure\"]\n", + " },\n", + " \"data_schema\": {\n", + " \"PRODUCT\": {\n", + " \"dtype\": \"string\"\n", + " },\n", + " \"PRICE\": {\n", + " \"dtype\": \"numeric\"\n", + " },\n", + " \"ORDERDATE\": {\n", + " \"dtype\": \"date\"\n", + " },\n", + " \"ID\": {\n", + " \"dtype\": \"string\"\n", + " },\n", + " \"COSTCENTER\": {\n", + " \"dtype\": \"string\"\n", + " }\n", + " }\n", + "}\n", + "\n", + "response = client.predict(body=example_request_by_columns_dict, model_name=\"sap-rpt-1-small\")\n", + "print(response.predictions)" + ], + "id": "ac9f2b7e705eb61", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Example of async usage of RPTClient\n", + "\n", + "The `RPTClient` also supports asynchronous calls by `apredict` method." + ], + "id": "240f50b4599701f3" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "await client.apredict(body=example_request_by_columns_dict, model_name=\"sap-rpt-1-small\")", + "id": "7ec3315cb2c29efa", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(unsupported_models)=\n", + "# Using New Models Before Official SDK Support\n", + "\n", + "You can use models via Gen AI Hub even before they are officially listed, provided their provider family (e.g., `Google`, `Amazon Bedrock`) is supported.\n", + "\n", + "1. **Native SDK Clients:**\n", + "\n", + " If using the provider's native SDK (like `boto3`, `google-genai`) through the Gen AI Hub proxy, you can often use the new model name/ID directly with existing client methods.\n", + "\n", + "2. **Langchain Integration (`init_llm`):**\n", + "\n", + " The `init_llm` helper simplifies creating Langchain LLM objects configured for the proxy.\n", + "\n", + " * **Alternative:** You can always bypass `init_llm` and instantiate the Langchain classes (e.g., `ChatGoogleGenerativeAI`, `ChatBedrock`, `ChatBedrockConverse`) directly.\n", + " * **Bedrock Specifics**:\n", + " * Requires `model_id` in addition to `model_name`. Find IDs [here](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html). `init_llm` automatically selects the appropriate Bedrock API (older Invoke via `ChatBedrock` or newer Converse via `ChatBedrockConverse`) based on known models.\n", + " * **Crucially:** For *new* Bedrock models or to force a specific API (Invoke/Converse), you must pass the corresponding initialization function (`init_chat_model` or `init_chat_converse_model`) to the `init_func` argument of `init_llm`.\n" + ], + "id": "5e85a5c1bfbfe321" + }, + { + "cell_type": "code", + "id": "f6676f9c4f07cb2d", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy.langchain import init_llm\n", + "# Import specific init functions for overriding Bedrock behavior\n", + "from gen_ai_hub.proxy.langchain.amazon import (\n", + " init_chat_model as amazon_init_invoke_model,\n", + " init_chat_converse_model as amazon_init_converse_model\n", + ")\n", + "from gen_ai_hub.proxy.langchain.google_genai import init_chat_model as google_genai_init_chat_model\n", + "\n", + "# --- Google Example ---\n", + "llm_google = init_llm(model_name='gemini-newer-version', init_func=google_genai_init_chat_model) # Often just needs model_name\n", + "\n", + "# --- Bedrock Example (New Model requiring Converse API) ---\n", + "model_name_amazon = 'anthropic--claude-newer-version'\n", + "model_id_amazon = 'anthropic.claude-newer-version-v1:0' # Use actual ID\n", + "\n", + "llm_amazon = init_llm(\n", + " model_name_amazon,\n", + " model_id=model_id_amazon,\n", + " init_func=amazon_init_converse_model # Explicitly select Converse API\n", + ")\n", + "\n", + "# --- Bedrock Example (Explicitly using older Invoke API) ---\n", + "# llm_amazon_invoke = init_llm(\n", + "# 'some-model-name',\n", + "# model_id='some-model-id',\n", + "# init_func=amazon_init_invoke_model # Explicitly select Invoke API\n", + "# )\n" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/metering.ipynb b/packages/gen/docs/gen_ai_hub/examples/metering.ipynb new file mode 100644 index 0000000..990a015 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/metering.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dfac10b4de0cc153", + "metadata": {}, + "source": [ + "# Metering for AI Units\n", + "\n", + "You have AI functionality in your application that is commercialised using AI units.\n", + "\n", + "Your application thus needs to report a business metrics that reflects the business value of that AI functionality and covers the cost caused by GenAI / LLM services, e.g. processed documents. \n", + "\n", + "The consumption of AI units per customer tenant and the underlying business metrics should be reflected in billing and for customer transparency in SAP4ME.\n", + "\n", + "For further information see: [Metering for AI Units](https://workzone.one.int.sap/site#workzone-home&/wiki/show/PjZrYMoGizdSSi1iJ68Plq)" + ] + }, + { + "cell_type": "markdown", + "id": "78fe130ea4d0b92e", + "metadata": {}, + "source": [ + "## Metering using the Generative AI Hub\n", + "\n", + "The generative AI hub is a central piece of the GenAI architecture. \n", + "\n", + "If it is used directly by the application and the business metrics definition follows certain supported patterns, the generative AI hub can be used to report business metrics to Unified Metering on behalf of the application. \n", + "\n", + "Each LLM access request to the generative AI hub needs to supply additional information in the request as documented here: [Documentation for providing additional metering relevant metadata in an LLM request](https://help.sap.com/doc/4317866f8cb44a089995b8444dc6c707/INTERNAL/en-US/553250b6ec764a05be43a7cd8cba0526.pdf) (page 10).\n", + "\n", + "The headers required for metering are:\n", + "- `X-USECASE-ID`: \"identifier\"\n", + "- `X-BUSINESS-CONTEXT`: \"value\"\n", + "- `X-LOCALTENANT-ID`: \"unique identifier\" \n", + "- `X-PRODUCT-TYPE`: \"value\"" + ] + }, + { + "cell_type": "markdown", + "id": "dec63a57b2bc0091", + "metadata": {}, + "source": [ + "## Enabling Metering using the SDK\n", + "\n", + "**There are two ways to set headers for metering:**\n", + "\n", + "1. **Instance-level headers**: Applied to all requests made by the proxy client instance.\n", + "\n", + "2. **Request-level headers**: Applied only to requests within a context manager block." + ] + }, + { + "cell_type": "markdown", + "id": "metering-headers-setup", + "metadata": {}, + "source": [ + "### Common Setup\n", + "\n", + "Define your metering headers once and reuse them across different clients:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "common-setup", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.proxy.gen_ai_hub_proxy import temporary_headers_addition\n", + "\n", + "METERING_HEADERS = {\n", + " 'X-USECASE-ID': 'my-usecase',\n", + " 'X-BUSINESS-CONTEXT': 'my-context',\n", + " 'X-LOCALTENANT-ID': 'tenant-123',\n", + " 'X-PRODUCT-TYPE': 'my-product'\n", + "}\n", + "\n", + "# Create a proxy client with instance-level headers\n", + "proxy_client = get_proxy_client('gen-ai-hub')\n", + "proxy_client.set_headers_addition(headers=METERING_HEADERS)" + ] + }, + { + "cell_type": "markdown", + "id": "6e6c2745d354dceb", + "metadata": {}, + "source": [ + "---\n", + "## Native LLM Clients\n", + "\n", + "### Instance-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8255f910c5e42df4", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.openai import OpenAI\n", + "\n", + "# All requests from this client include metering headers\n", + "client = OpenAI(proxy_client=proxy_client)\n", + "response = client.chat.completions.create(\n", + " model='gpt-4o',\n", + " messages=[{'role': 'user', 'content': 'Hello!'}]\n", + ")\n", + "print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "3d280dc40a20b376", + "metadata": {}, + "source": [ + "### Request-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff28fbc76e89bdd4", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.openai import OpenAI\n", + "\n", + "client = OpenAI()\n", + "\n", + "# Only this request includes metering headers\n", + "with temporary_headers_addition(headers=METERING_HEADERS):\n", + " response = client.chat.completions.create(\n", + " model='gpt-4o',\n", + " messages=[{'role': 'user', 'content': 'Hello!'}]\n", + " )\n", + " print(response.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "orchestration-section", + "metadata": {}, + "source": [ + "---\n", + "## Orchestration Service\n", + "\n", + "### Instance-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "orchestration-instance", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration_v2 import (OrchestrationService, OrchestrationConfig, ModuleConfig, Template,\n", + " PromptTemplatingModuleConfig, LLMModelDetails, UserMessage)\n", + "\n", + "config = OrchestrationConfig(\n", + " modules=ModuleConfig(\n", + " prompt_templating=PromptTemplatingModuleConfig(\n", + " prompt=Template(template=[UserMessage(content='{{?input}}')]),\n", + " model=LLMModelDetails(name='gpt-4o')\n", + " )\n", + " )\n", + ")\n", + "\n", + "# All requests from this service include metering headers\n", + "service = OrchestrationService(proxy_client=proxy_client, config=config)\n", + "response = service.run(placeholder_values={'input': 'Hello!'})\n", + "print(response.final_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "orchestration-request", + "metadata": {}, + "source": [ + "### Request-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "orchestration-temp", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration_v2 import OrchestrationService\n", + "\n", + "service = OrchestrationService(config=config)\n", + "\n", + "# Only this request includes metering headers\n", + "with temporary_headers_addition(headers=METERING_HEADERS):\n", + " response = service.run(placeholder_values={'input': 'Hello!'})\n", + " print(response.final_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "prompt-registry-section", + "metadata": {}, + "source": [ + "---\n", + "## Prompt Registry\n", + "\n", + "### Instance-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "prompt-registry-instance", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.prompt_registry import PromptTemplateClient\n", + "\n", + "# All requests from this client include metering headers\n", + "client = PromptTemplateClient(proxy_client=proxy_client)\n", + "templates = client.get_prompt_templates(scenario='my-scenario')\n", + "print(f\"Found {templates.count} templates\")" + ] + }, + { + "cell_type": "markdown", + "id": "prompt-registry-request", + "metadata": {}, + "source": [ + "### Request-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "prompt-registry-temp", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.prompt_registry import PromptTemplateClient\n", + "\n", + "client = PromptTemplateClient()\n", + "\n", + "# Only this request includes metering headers\n", + "with temporary_headers_addition(headers=METERING_HEADERS):\n", + " templates = client.get_prompt_templates(scenario='my-scenario')\n", + " print(f\"Found {templates.count} templates\")" + ] + }, + { + "cell_type": "markdown", + "id": "grounding-section", + "metadata": {}, + "source": [ + "---\n", + "## Document Grounding Clients\n", + "\n", + "The document grounding clients (`PipelineAPIClient`, `RetrievalAPIClient`, `VectorAPIClient`) support the same header injection pattern.\n", + "\n", + "### Instance-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "grounding-instance", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.document_grounding import PipelineAPIClient, RetrievalAPIClient, VectorAPIClient\n", + "\n", + "# All requests from these clients include metering headers\n", + "pipeline_client = PipelineAPIClient(proxy_client=proxy_client)\n", + "retrieval_client = RetrievalAPIClient(proxy_client=proxy_client)\n", + "vector_client = VectorAPIClient(proxy_client=proxy_client)\n", + "\n", + "pipelines = pipeline_client.get_pipelines()\n", + "repositories = retrieval_client.get_data_repositories()\n", + "collections = vector_client.get_collections()\n", + "\n", + "print(f\"Found {pipelines.count} pipelines\")\n", + "print(f\"Found {repositories.count} repositories\")\n", + "print(f\"Found {collections.count} collections\")" + ] + }, + { + "cell_type": "markdown", + "id": "grounding-request", + "metadata": {}, + "source": [ + "### Request-level headers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "grounding-temp", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.document_grounding import RetrievalAPIClient\n", + "\n", + "client = RetrievalAPIClient()\n", + "\n", + "# Only this request includes metering headers\n", + "with temporary_headers_addition(headers=METERING_HEADERS):\n", + " repositories = client.get_data_repositories()\n", + " print(f\"Found {repositories.count} repositories\")" + ] + }, + { + "cell_type": "markdown", + "id": "combining-headers", + "metadata": {}, + "source": [ + "---\n", + "## Combining Instance and Request-level Headers\n", + "\n", + "Request-level headers are merged with instance-level headers. If the same header is set at both levels, the request-level value takes precedence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "combining-example", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.openai import OpenAI\n", + "\n", + "# Set instance-level headers\n", + "proxy_client.set_headers_addition({\n", + " 'X-USECASE-ID': 'default-usecase',\n", + " 'X-LOCALTENANT-ID': 'tenant-123'\n", + "})\n", + "\n", + "client = OpenAI(proxy_client=proxy_client)\n", + "\n", + "# Override X-USECASE-ID for this specific request\n", + "with temporary_headers_addition({'X-USECASE-ID': 'special-usecase'}):\n", + " # Request will have:\n", + " # X-USECASE-ID: 'special-usecase' (from request-level)\n", + " # X-LOCALTENANT-ID: 'tenant-123' (from instance-level)\n", + " response = client.chat.completions.create(\n", + " model='gpt-4o',\n", + " messages=[{'role': 'user', 'content': 'Hello!'}]\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/orchestration-service.ipynb b/packages/gen/docs/gen_ai_hub/examples/orchestration-service.ipynb new file mode 100644 index 0000000..d265664 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/orchestration-service.ipynb @@ -0,0 +1,1702 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d4d8f0d470dd5ce2", + "metadata": {}, + "source": [ + "(orchestration)=\n", + "# Orchestration Service [Deprecated]" + ] + }, + { + "cell_type": "markdown", + "id": "5cfa2bb46ec75e47", + "metadata": {}, + "source": [ + "> **Important:** Note that version 1 of the orchestration service API is deprecated and will be decommissioned October 2026. Please refer to the [SAP Note 3634540](https://me.sap.com/notes/3634540). Use [](orchestration2)\n", + "\n", + "This notebook demonstrates how to use the SDK to interact with the Orchestration Service, enabling the creation of AI-driven workflows by seamlessly integrating various modules, such as templating, large language models (LLMs), data masking and content filtering. By leveraging these modules, you can build complex, automated workflows that enhance the capabilities of your AI solutions. For more details on configuring and using these modules, please refer to the [Orchestration Service Documentation](https://help.sap.com/docs/ai-launchpad/sap-ai-launchpad/orchestration)." + ] + }, + { + "cell_type": "markdown", + "id": "361c6244a294485e", + "metadata": {}, + "source": [ + "## Prerequisite\n", + "\n", + "> **Important:** Before you begin using the SDK, make sure to set up a virtual deployment of the Orchestration Service.\n", + "\n", + "For detailed guidance on setting up the Orchestration Service, please refer to the setup guide [here](https://help.sap.com/docs/ai-launchpad/sap-ai-launchpad/create-deployment-for-orchestration)." + ] + }, + { + "cell_type": "markdown", + "id": "6908905a80bdff3f", + "metadata": {}, + "source": [ + "## Authentication\n", + "\n", + "By default, the `OrchestrationService` initializes a `GenAIHubProxyClient`, which automatically configures credentials using configuration files or environment variables, as outlined in the *Introduction* section.\n", + "\n", + "If you prefer to set credentials manually, you can provide a custom instance using the `proxy_client` parameter." + ] + }, + { + "cell_type": "markdown", + "id": "13691a6f5ffee555", + "metadata": {}, + "source": [ + "## Basic Orchestration Pipeline\n", + "\n", + "Let's walk through a basic orchestration pipeline for a translation task." + ] + }, + { + "cell_type": "markdown", + "id": "a59c47b7e4c0adfe", + "metadata": {}, + "source": [ + "### Step 1: Define the Template and Default Input Values\n", + "\n", + "The `Template` class is used to define structured message templates for generating dynamic interactions with language models. In this example, the template is designed for a translation assistant, allowing users to specify a language and text for translation." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9a59d3a2a5266df5", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T17:19:21.085674Z", + "start_time": "2025-09-13T17:19:21.083151Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "\n", + "template = Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful translation assistant.\"),\n", + " UserMessage(\n", + " \"Translate the following text to {{?to_lang}}: {{?user_query}}\"\n", + " ),\n", + " ],\n", + " defaults=[\n", + " TemplateValue(name=\"to_lang\", value=\"German\"),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ddd21a6c7c9db50e", + "metadata": {}, + "source": [ + "This template can be used to create translation requests where the language and text to be translated are specified dynamically. The placeholders in the `UserMessage` will be replaced with the actual values provided at runtime, and the default value for the language is set to German." + ] + }, + { + "cell_type": "markdown", + "id": "1efb1b8a33d12de9", + "metadata": {}, + "source": [ + "### Step 2: Define the LLM\n", + "\n", + "The `LLM` class is used to configure and initialize a language model for generating text based on specific parameters. In this example, we'll use the `gpt-4o` model to perform the translation task.\n", + "\n", + "**Note:** The Orchestration Service automatically manages the virtual deployment of the language model, so no additional setup is needed on your end." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cf3356f0268f267", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T17:25:24.611476Z", + "start_time": "2025-09-13T17:25:24.609129Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "\n", + "llm = LLM(name=\"gpt-5-nano\", parameters={\"max_completion_tokens\": 512})" + ] + }, + { + "cell_type": "markdown", + "id": "7a1ad3e8f1cf6263", + "metadata": {}, + "source": [ + "Initializes the language model to use the `gpt-5-nano` model. It will generate responses up to 512 tokens in length." + ] + }, + { + "cell_type": "markdown", + "id": "c97ee26cf9c303b7", + "metadata": { + "tags": [] + }, + "source": [ + "### Step 3: Create the Orchestration Configuration\n", + "\n", + "The `OrchestrationConfig` class defines a configuration for integrating various modules, such as templates and language models, into a cohesive orchestration setup. It specifies how these components interact and are configured to achieve the desired operational scenario." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5126393eb5881e9c", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T17:25:27.649225Z", + "start_time": "2025-09-13T17:25:27.647149Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "\n", + "config = OrchestrationConfig(\n", + " template=template,\n", + " llm=llm,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "26802bca242e40e0", + "metadata": {}, + "source": [ + "### Step 4: Run the Orchestration Request\n", + "\n", + "The `OrchestrationService` class is used to interact with a orchestration service instance by providing configuration details to initiate and manage its operations." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c7c7a3b8cfef44c1", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T17:25:30.761708Z", + "start_time": "2025-09-13T17:25:30.746534Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "\n", + "orchestration_service = OrchestrationService(config=config)" + ] + }, + { + "cell_type": "markdown", + "id": "9b5a114f96ccea5b", + "metadata": {}, + "source": [ + "Call the `run` method with the required `template values`. The service will process the input according to the configuration and return the result." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d2087cffd3e8d5e3", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T17:26:10.458616Z", + "start_time": "2025-09-13T17:26:08.378Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Der Orchestrierungsdienst funktioniert!\n" + ] + } + ], + "source": [ + "result = orchestration_service.run(template_values=[\n", + " TemplateValue(name=\"user_query\", value=\"The Orchestration Service is working!\"),\n", + "])\n", + "print(result.orchestration_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "5401036759b4bfe2", + "metadata": {}, + "source": [ + "#### Referencing Templates in the Prompt Registry\n", + " In Step 3 you can also use a prompt template reference, which allows you to reuse existing templates stored in the Prompt Registry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bdfc9e11126e954", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.template_ref import TemplateRef\n", + "\n", + "template_by_id = TemplateRef.from_id(prompt_template_id=\"648871d9-b207-441c-8c13-afee71b0dbec\") # this is just an example id\n", + "template_by_names = TemplateRef.from_tuple(scenario=\"translation\", name=\"translate_text\", version=\"0.1.0\")" + ] + }, + { + "cell_type": "markdown", + "id": "72d37fd8", + "metadata": {}, + "source": [ + "#### Overview of response_format Parameter Options\n", + "\n", + "The `response_format` parameter allows the model output to be formatted in several predefined ways, as follows:\n", + "\n", + "1. **text**: This is the simplest form where the model's output is generated as plain text. It is suitable for applications that require raw text processing.\n", + "\n", + "2. **json_object**: Under this setting, the model's output is structured as a JSON object. This is useful for applications that handle data in JSON format, enabling easy integration with web applications and APIs.\n", + "\n", + "3. **json_schema**: This setting allows the model's output to adhere to a defined JSON schema. This is particularly useful for applications that require strict data validation, ensuring the output matches a predefined schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99fdcfca", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "\n", + "template = Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful translation assistant.\"),\n", + " UserMessage(\"{{?user_query}}\")\n", + " ],\n", + " response_format=\"text\",\n", + " defaults=[\n", + " TemplateValue(name=\"user_query\", value=\"Who was the first person on the moon?\")\n", + " ]\n", + ")\n", + "\n", + "# Response:\n", + "# The first man on the moon was Neil Armstrong." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bdadd9e", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "\n", + "template = Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful translation assistant. Format the response as json.\"),\n", + " UserMessage(\"{{?user_query}}\")\n", + " ],\n", + " response_format=\"json_object\",\n", + " defaults=[\n", + " TemplateValue(name=\"user_query\", value=\"Who was the first person on the moon?\")\n", + " ]\n", + ")\n", + "\n", + "# Response:\n", + "# {\n", + "# \"First_man_on_the_moon\": \"Neil Armstrong\"\n", + "# }" + ] + }, + { + "cell_type": "markdown", + "id": "d58230bd", + "metadata": {}, + "source": [ + "**Important:** When using `response_format` as json_object, ensure that messages contain the word 'json' in some form." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7aea55a5", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "from gen_ai_hub.orchestration.models.response_format import ResponseFormatJsonSchema\n", + "\n", + "json_schema = {\n", + " \"title\": \"Person\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"firstName\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The person's first name.\"\n", + " },\n", + " \"lastName\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The person's last name.\"\n", + " }\n", + " }\n", + "}\n", + "template = Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful translation assistant.\"),\n", + " UserMessage(\"{{?user_query}}\")\n", + " ],\n", + " response_format = ResponseFormatJsonSchema(name=\"person\", description=\"person mapping\", schema=json_schema),\n", + " defaults=[\n", + " TemplateValue(name=\"user_query\", value=\"Who was the first person on the moon?\")\n", + " ]\n", + ")\n", + "\n", + "# Response:\n", + "# {\n", + "# \"firstName\": \"Neil\",\n", + "# \"lastName\": \"Armstrong\"\n", + "# }" + ] + }, + { + "cell_type": "markdown", + "id": "b6f4afb00a8bb385", + "metadata": {}, + "source": [ + "## Understanding Deployment Resolution\n", + "\n", + "The `OrchestrationService` class provides multiple ways to specify and target orchestration deployments when sending requests. Below are the available options:" + ] + }, + { + "cell_type": "markdown", + "id": "8669ba0abbcc8e48", + "metadata": {}, + "source": [ + "### Default Behavior\n", + "\n", + "If no parameters are provided, the `OrchestrationService` automatically searches for a `RUNNING` deployment. If multiple running deployments exist, the service selects the most recently created one." + ] + }, + { + "cell_type": "markdown", + "id": "beb881b8e2e3a393", + "metadata": {}, + "source": [ + "### Direct Deployment Specification\n", + "\n", + "You can explicitly define the target deployment using the following options:\n", + "\n", + "1. **API URL** (`api_url`):\n", + " - Specify the exact URL assigned to the deployment during its creation.\n", + " - Refer to the Prerequisites section for more details on obtaining the deployment URL.\n", + "\n", + "2. **Deployment ID** (`deployment_id`):\n", + " - Use the unique identifier assigned to the deployment instead of the URL." + ] + }, + { + "cell_type": "markdown", + "id": "9c0a62dbfe5c22b", + "metadata": {}, + "source": [ + "### Config-Based Specification\n", + "\n", + "If you want to target deployments based on their configuration source, use one of the following options:\n", + "\n", + "1. **Configuration ID** (`config_id`):\n", + " - The `OrchestrationService` searches for a `RUNNING` deployment created using the provided configuration ID.\n", + "\n", + "2. **Configuration Name** (`config_name`):\n", + " - The service looks for a `RUNNING` deployment that matches the specified configuration name.\n", + "\n", + "If multiple deployments match the given configuration criteria, the most recently created one will be selected automatically." + ] + }, + { + "cell_type": "markdown", + "id": "994a9645b29ef1db", + "metadata": {}, + "source": [ + "## Optional Modules" + ] + }, + { + "cell_type": "markdown", + "id": "d92be02fe6eea0b2", + "metadata": {}, + "source": [ + "### Data Masking\n", + "\n", + "The `Data Masking` module `anonymizes` or `pseudonymizes` personally identifiable information (PII) before it is processed by the LLM module. Currently, `SAPDataPrivacyIntegration` is the only available masking provider.\n", + "\n", + "#### Masking Types\n", + "\n", + "- **Anonymization**: All identifying information is replaced with placeholders (e.g., MASKED_ENTITY), and the original data cannot be recovered, ensuring that no trace of the original information is retained.\n", + "- **Pseudonymization**: Data is substituted with unique placeholders (e.g., MASKED_ENTITY_ID), allowing the original information to be restored if needed.\n", + "\n", + "In both cases, the masking module identifies sensitive data and replaces it with appropriate placeholders before further processing.\n", + "\n", + "\n", + "#### Configuration Options\n", + "\n", + "- **entities**: Specify which types of entities to mask (e.g., EMAIL, PHONE, PERSON).\n", + "- **allowlist**: Provide specific terms or patterns that should be excluded from masking, even if they match entity types.\n", + "- **mask_grounding_input**: When enabled, ensures that masking is also applied to the context provided to the grounding module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55de89ea8090d7eb", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.utils import load_text_file\n", + "from gen_ai_hub.orchestration.models.data_masking import DataMasking\n", + "from gen_ai_hub.orchestration.models.sap_data_privacy_integration import SAPDataPrivacyIntegration, MaskingMethod, \\\n", + " ProfileEntity\n", + "\n", + "orchestration_service = OrchestrationService()\n", + "\n", + "data_masking = DataMasking(\n", + " providers=[\n", + " SAPDataPrivacyIntegration(\n", + " method=MaskingMethod.ANONYMIZATION, # or MaskingMethod.PSEUDONYMIZATION\n", + " entities=[\n", + " ProfileEntity.EMAIL,\n", + " ProfileEntity.PHONE,\n", + " ProfileEntity.PERSON,\n", + " ProfileEntity.ORG,\n", + " ProfileEntity.LOCATION\n", + " ],\n", + " allowlist=[\"M&K Group\"], # Terms to exclude from masking\n", + " )\n", + " ]\n", + ")\n", + "\n", + "config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful AI assistant.\"),\n", + " UserMessage(\"Summarize the following CV in 10 sentences: {{?orgCV}}\"),\n", + " ]\n", + " ),\n", + " llm=LLM(\n", + " name=\"gpt-4o\",\n", + " ),\n", + " data_masking=data_masking\n", + ")\n", + "\n", + "cv_as_string = load_text_file(\"data/cv.txt\")\n", + "\n", + "result = orchestration_service.run(\n", + " config=config,\n", + " template_values=[\n", + " TemplateValue(name=\"orgCV\", value=cv_as_string)\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e44d871bc49bb3bc", + "metadata": {}, + "outputs": [], + "source": [ + "print(result.orchestration_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "6beb1e933ba1eada", + "metadata": {}, + "source": [ + "### Content Filtering\n", + "\n", + "The `Content Filtering` module can be configured to filter both the `input` to the LLM module (input filter) and the `output` generated by the LLM (output filter). The module uses predefined classification services to detect inappropriate or unwanted content. Azure Content Filter sensitivity is controlled by customizable `thresholds`, assuring the content aligns with the desired standards before processing or generating as output. Llama Guard 3 Filter, equipped with 14 categories, runs on a binary mechanism, accepting only true or false. Setting a category to true enables filtering for it. It's possible to execute both filters in a single request, optimizing efficiency." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ff60002292d04a4", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.content_filtering import ContentFiltering,InputFiltering, OutputFiltering\n", + "from gen_ai_hub.orchestration.models.azure_content_filter import AzureContentFilter, AzureThreshold\n", + "from gen_ai_hub.orchestration.models.llama_guard_3_filter import LlamaGuard38bFilter\n", + "\n", + "input_filter= AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE,\n", + " violence=AzureThreshold.ALLOW_SAFE,\n", + " self_harm=AzureThreshold.ALLOW_SAFE,\n", + " sexual=AzureThreshold.ALLOW_SAFE)\n", + "input_filter_llama = LlamaGuard38bFilter(hate=True)\n", + "output_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE,\n", + " violence=AzureThreshold.ALLOW_SAFE_LOW,\n", + " self_harm=AzureThreshold.ALLOW_SAFE_LOW_MEDIUM,\n", + " sexual=AzureThreshold.ALLOW_ALL)\n", + "output_filter_llama = LlamaGuard38bFilter(hate=True)\n", + "\n", + "config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful AI assistant.\"),\n", + " UserMessage(\"{{?text}}\"),\n", + " ]\n", + " ),\n", + " llm=LLM(\n", + " name=\"gpt-4o\",\n", + " ),\n", + " filtering=ContentFiltering(\n", + " input_filtering=InputFiltering(filters=[input_filter, input_filter_llama]),\n", + " output_filtering=OutputFiltering(filters=[output_filter, output_filter_llama])\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc2bc3e0e569ada3", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.exceptions import OrchestrationError\n", + "\n", + "try:\n", + " result = orchestration_service.run(config=config, template_values=[\n", + " TemplateValue(name=\"text\", value=\"I hate you\")\n", + " ])\n", + "except OrchestrationError as error:\n", + " print(error.message)" + ] + }, + { + "cell_type": "markdown", + "id": "1b86bc265d44e0b", + "metadata": {}, + "source": [ + "## Streaming\n", + "\n", + "When you initiate an orchestration request, the full response is typically processed and delivered in one go. For longer responses, this can lead to delays in receiving the complete output. To mitigate this, you have the option to stream the results as they are being generated. This helps in rapidly processing or displaying initial portions of the results without waiting for the entire computation to finish.\n" + ] + }, + { + "cell_type": "markdown", + "id": "20ece46940e0a371", + "metadata": {}, + "source": [ + "To activate streaming, use the `stream` method of the `OrchestrationService`. This method returns an object that streams chunks of the response as they become available. You can then extract relevant information from the `delta` field.\n", + "\n", + "Here's how you can set up a simple configuration to stream orchestration results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "963634b5982f055e", + "metadata": {}, + "outputs": [], + "source": [ + "config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful AI assistant.\"),\n", + " UserMessage(\"{{?text}}\"),\n", + " ]\n", + " ),\n", + " llm=LLM(\n", + " name=\"gpt-4o-mini\",\n", + " parameters={\n", + " \"max_completion_tokens\": 256,\n", + " \"temperature\": 0.0\n", + " }\n", + " ),\n", + ")\n", + "\n", + "service = OrchestrationService()\n", + "\n", + "response = service.stream(\n", + " config=config,\n", + " template_values=[\n", + " TemplateValue(name=\"text\", value=\"Which color is the sky? Answer in one sentence.\")\n", + " ]\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk.orchestration_result)\n", + " print(\"*\" * 20)" + ] + }, + { + "cell_type": "markdown", + "id": "51c8b188492f64", + "metadata": {}, + "source": [ + "**Note:** As shown above, streaming responses contain a delta field instead of a message field." + ] + }, + { + "cell_type": "markdown", + "id": "9eb17f922ba86c60", + "metadata": {}, + "source": [ + "You can customize the global stream behavior by setting options like `chunk_size` which controls the amount of data processed in each chunk:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d42b1e5f422ad6d1", + "metadata": {}, + "outputs": [], + "source": [ + "response = service.stream(\n", + " config=config,\n", + " template_values=[\n", + " TemplateValue(name=\"text\", value=\"Which color is the sky? Answer in one sentence.\")\n", + " ],\n", + " stream_options={\n", + " 'chunk_size': 25\n", + " }\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk.orchestration_result)\n", + " print(\"*\" * 20)" + ] + }, + { + "cell_type": "markdown", + "id": "f6635d35021f21d", + "metadata": {}, + "source": [ + "Modules that influence or process streaming results, such as `OutputFiltering`, might need specific stream options. The `overlap` option allows you to include extra context during the filtering process:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75bfce9c0c694bd1", + "metadata": {}, + "outputs": [], + "source": [ + "config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful AI assistant.\"),\n", + " UserMessage(\"{{?text}}\"),\n", + " ]\n", + " ),\n", + " llm=LLM(\n", + " name=\"gpt-4o-mini\",\n", + " parameters={\n", + " \"max_completion_tokens\": 256,\n", + " \"temperature\": 0.0\n", + " }\n", + " ),\n", + " output_filtering=OutputFiltering(\n", + " filters=[AzureContentFilter(\n", + " hate=AzureThreshold.ALLOW_ALL,\n", + " violence=AzureThreshold.ALLOW_ALL,\n", + " self_harm=AzureThreshold.ALLOW_ALL,\n", + " sexual=AzureThreshold.ALLOW_ALL\n", + " )\n", + " ],\n", + " stream_options={ 'overlap': 100 }\n", + " )\n", + ")\n", + "\n", + "response = service.stream(\n", + " config=config,\n", + " template_values=[\n", + " TemplateValue(name=\"text\", value=\"Why is the sky blue?\")\n", + " ]\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk.orchestration_result.choices[0].delta.content, end='')\n" + ] + }, + { + "cell_type": "markdown", + "id": "f545226e048451c6", + "metadata": {}, + "source": [ + "## Tool Calling (Function Calling)\n", + "\n", + "The Orchestration Service supports **tool calling**, which allows large language models (LLMs) to request the execution of external operations such as Python functions, API calls, or other tools as part of their workflow.\n", + "\n", + "This feature enables you to build advanced AI solutions that can perform calculations, access data, or interact with external systems in response to user input.\n", + "\n", + "---\n", + "\n", + "### Defining Tools\n", + "\n", + "You can define tools in several ways, depending on your requirements and the level of control you need.\n", + "\n", + "#### Using the Python Decorator\n", + "\n", + "The simplest way to define a tool is to decorate a Python function with `@function_tool()`. The function’s signature and docstring are used to describe the tool to the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4755d6d389eae7b4", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.tools import function_tool\n", + "\n", + "@function_tool()\n", + "def multiply(a: int, b: int) -> int:\n", + " \"\"\"Multiply two numbers.\"\"\"\n", + " return a * b\n", + "\n", + "@function_tool()\n", + "def add(a: int, b: int) -> int:\n", + " \"\"\"Add two numbers.\"\"\"\n", + " return a + b\n", + "\n", + "tools = [multiply, add]" + ] + }, + { + "cell_type": "markdown", + "id": "a04cd2cdb5fd9229", + "metadata": {}, + "source": [ + "#### Using the `FunctionTool` Class\n", + "\n", + "For more control, you can use the `FunctionTool` class directly. This is useful if you want to customize the schema, enable strict argument checking, or wrap an existing function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f27847c", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.tools import FunctionTool\n", + "\n", + "def get_weather(location: str) -> str:\n", + " \"\"\"Get current temperature for a given location.\"\"\"\n", + " # Replace with your actual implementation\n", + " return \"22°C\"\n", + "\n", + "weather_tool = FunctionTool(\n", + " name=\"get_weather\",\n", + " description=\"Get current temperature for a given location.\",\n", + " parameters={\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City and country e.g. BogotÃĄ, Colombia\"\n", + " }\n", + " },\n", + " \"required\": [\"location\"],\n", + " \"additionalProperties\": False\n", + " },\n", + " strict=True,\n", + " function=get_weather\n", + ")\n", + "\n", + "tools = [weather_tool]" + ] + }, + { + "cell_type": "markdown", + "id": "f824a172", + "metadata": {}, + "source": [ + "You can also create a `FunctionTool` from a function using the `from_function` static method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "356b01a1", + "metadata": {}, + "outputs": [], + "source": [ + "weather_tool = FunctionTool.from_function(get_weather, strict=True)\n", + "tools = [weather_tool]" + ] + }, + { + "cell_type": "markdown", + "id": "8014e8f7", + "metadata": {}, + "source": [ + "#### Using a JSON Schema Dictionary\n", + "\n", + "You can define a tool directly as a JSON schema dictionary. This is useful if you want to specify the tool interface without implementing the function in Python, or if you want to integrate with external systems." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01c36ac6", + "metadata": {}, + "outputs": [], + "source": [ + "tools = [{\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_weather\",\n", + " \"description\": \"Get current temperature for a given location.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City and country e.g. BogotÃĄ, Colombia\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"location\"\n", + " ],\n", + " \"additionalProperties\": False\n", + " },\n", + " \"strict\": True\n", + " }\n", + "}]" + ] + }, + { + "cell_type": "markdown", + "id": "984a4709", + "metadata": {}, + "source": [ + "You can then attach any of these tool definitions to your template:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6661860", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.template import Template\n", + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "\n", + "template = Template(\n", + " messages=[\n", + " SystemMessage(\"You are a weather assistant.\"),\n", + " UserMessage(\"What is the temperature in {{?location}}?\"),\n", + " ],\n", + " tools=tools,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8612f16e", + "metadata": {}, + "source": [ + "### Synchronous Tool Call Workflow\n", + "\n", + "When the LLM decides to call a tool, the orchestration response will include a `tool_calls` field. You are responsible for executing the tool(s), adding the results to the conversation history, and running the orchestration again to get the final answer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94bd56b1", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.template import TemplateValue\n", + "from gen_ai_hub.orchestration.models.message import Message, ToolMessage\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "\n", + "# Assume 'template' and 'weather_tool' are defined as above\n", + "llm = LLM(name=\"gpt-4o-mini\", parameters={\"max_completion_tokens\": 200, \"temperature\": 0.0})\n", + "config = OrchestrationConfig(template=template, llm=llm)\n", + "template_values = [TemplateValue(name=\"location\", value=\"BogotÃĄ, Colombia\")]\n", + "\n", + "# First run: triggers tool call\n", + "service = OrchestrationService()\n", + "response = service.run(config=config, template_values=template_values)\n", + "tool_calls = response.orchestration_result.choices[0].message.tool_calls\n", + "\n", + "# Execute tool(s) and build new history\n", + "history: List[Message] = []\n", + "history.extend(response.module_results.templating)\n", + "history.append(response.orchestration_result.choices[0].message)\n", + "\n", + "for tool_call in tool_calls:\n", + " # For FunctionTool, use .execute(**tool_call.function.parse_arguments())\n", + " result = weather_tool.execute(**tool_call.function.parse_arguments())\n", + " tool_message = ToolMessage(\n", + " content=str(result),\n", + " tool_call_id=tool_call.id,\n", + " )\n", + " history.append(tool_message)\n", + "\n", + "# Second run: LLM receives tool result and produces final answer\n", + "response2 = service.run(\n", + " config=config,\n", + " template_values=template_values,\n", + " history=history,\n", + ")\n", + "print(response2.orchestration_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "f6ec0d33", + "metadata": {}, + "source": [ + "### Streaming Tool Calls\n", + "\n", + "When using streaming, tool calls may be split across multiple chunks. The `delta.tool_calls` field in each chunk contains partial or complete tool call information. You may need to buffer and concatenate arguments if they arrive in pieces." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cdb1b85", + "metadata": {}, + "outputs": [], + "source": [ + "# Assume 'config' and 'service' are defined as above\n", + "stream = service.stream(config=config, template_values=template_values)\n", + "final_tool_calls = {}\n", + "\n", + "for chunk in stream:\n", + " for tool_call in chunk.orchestration_result.choices[0].delta.tool_calls or []:\n", + " index = tool_call.index\n", + " if index not in final_tool_calls:\n", + " final_tool_calls[index] = tool_call\n", + " else:\n", + " # Concatenate arguments if split across chunks\n", + " final_tool_calls[index].function.arguments += tool_call.function.arguments\n", + "\n", + "# Now final_tool_calls contains all tool calls with complete arguments" + ] + }, + { + "cell_type": "markdown", + "id": "30eee9c74779ff36", + "metadata": {}, + "source": [ + "**âš ī¸ Note on Agentic Loop Support:**\n", + "\n", + "> The current SDK **does not provide built-in abstractions or convenience methods for managing the agentic loop** (the process of automatically handling tool call detection, execution, and iterative orchestration until a final answer is produced).\n", + ">\n", + "> As a user, you are responsible for:\n", + "> - Detecting tool calls in the LLM response\n", + "> - Executing the corresponding Python functions\n", + "> - Appending tool results to the conversation history (as `ToolMessage`)\n", + "> - Re-invoking the orchestration service as needed\n", + ">\n", + "> This approach gives you maximum flexibility, but you must implement the orchestration loop logic yourself." + ] + }, + { + "cell_type": "markdown", + "id": "33d37047", + "metadata": {}, + "source": [ + "## Using Images as Input\n", + "\n", + "The Orchestration Service supports multimodal prompts, enabling you to include images alongside text in your messages. This powerful feature unlocks a variety of applications, such as visual question answering (VQA), image captioning, object recognition, and generating text creatively based on visual input.\n", + "\n", + "This guide details how to prepare image inputs, integrate them into your prompts, and execute the orchestration to get insightful responses.\n", + "\n", + "### 1. Preparing Image Inputs\n", + "\n", + "To use an image, you first need to represent it as an `ImageItem` object. The `gen_ai_hub.orchestration.models.multimodal_items.ImageItem` class provides two convenient ways to do this:\n", + "\n", + "#### a) From a URL or Data URL\n", + "\n", + "This method is ideal for images hosted online or when you have the image data encoded as a Data URL (base64 encoded).\n", + "\n", + "* **Standard URL:** Provide a direct web link to the image file.\n", + "* **Data URL:** Provide the image data directly embedded in the URL string.\n", + "\n", + "**Note:** For web URLs, ensure the image is publicly accessible, as the service will need to fetch it." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "45e5788870c81362", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-04T18:49:14.383946Z", + "start_time": "2025-09-04T18:49:14.381441Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.multimodal_items import ImageItem\n", + "\n", + "# Example 1: Image from a standard, publicly accessible URL\n", + "# Ensure the URL points directly to the image file (e.g., .png, .jpg, ...)\n", + "image_from_web = ImageItem(url=\"https://picsum.photos/id/1/200/300\") # example image URL\n", + "\n", + "# Example 2: Image from a Data URL (base64-encoded)\n", + "# This is useful when you have the image content as a string.\n", + "# The format is \"data:[][;base64],\"\n", + "image_from_data_url = ImageItem(\n", + " url=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ed2bffcc7176ee52", + "metadata": {}, + "source": [ + "#### b) From a Local File\n", + "\n", + "If your image resides on your local filesystem, you can load it directly using the `ImageItem.from_file()` class method.\n", + "The `from_file` method handles opening, reading, and base64 encoding the image data for you, packaging it into an `ImageItem`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "794976e106d508b0", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.multimodal_items import ImageItem\n", + "\n", + "# Example: Image from a local file path\n", + "# To use this 'image_from_local_file' object, ensure it was successfully created.\n", + "try:\n", + " image_from_local_file = ImageItem.from_file(\"path/to/your/local/image.jpeg\")\n", + "except FileNotFoundError:\n", + " print(\"Error: The specified image file was not found.\")\n", + "except Exception as e:\n", + " print(f\"An error occurred while loading the image: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "453f9502", + "metadata": {}, + "source": [ + "### 2. Adding Images to a Prompt\n", + "\n", + "Once you have your `ImageItem` object(s), you can combine them with text to create a multimodal prompt. This is done by passing a list containing `ImageItem` instances and text strings to the `content` parameter of a `UserMessage`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4cb083d404897f7b", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-04T18:50:18.224403Z", + "start_time": "2025-09-04T18:50:05.956819Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "A laptop computer and a coffee cup.\n" + ] + } + ], + "source": [ + "from gen_ai_hub.orchestration.models.message import UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template\n", + "\n", + "# Simple visual question answering\n", + "content_vqa = [image_from_web, \"What objects are prominent in this image?\"]\n", + "\n", + "# Create a UserMessage with the mixed content\n", + "user_message = UserMessage(content=content_vqa)\n", + "\n", + "# Create a Template containing the UserMessage\n", + "prompt_template = Template(messages=[user_message])\n", + "\n", + "orchestration_service = OrchestrationService(config=OrchestrationConfig(template=prompt_template, llm=llm))\n", + "result = orchestration_service.run()\n", + "print(result.orchestration_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "a5e6da5b3b08e8d7", + "metadata": {}, + "source": [ + "## Translation\n", + "Translation module can be used to translate text from one language to another. You can use this module to translate input text before it is processed by the LLM module, or to translate the output generated by the LLM module. The translation module uses the SAP Document Translation service to perform the translation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afa8b564b77f7dda", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.translation.translation import InputTranslationConfig, OutputTranslationConfig\n", + "from gen_ai_hub.orchestration.models.translation.sap_document_translation import SAPDocumentTranslation\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.template import TemplateValue\n", + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "\n", + "\n", + "input_config = InputTranslationConfig(source_language=\"en-US\", target_language=\"de-DE\")\n", + "output_config = OutputTranslationConfig(source_language=\"de-DE\", target_language=\"en-US\")\n", + "\n", + "translation_module = SAPDocumentTranslation(\n", + " input_translation_config=input_config,\n", + " output_translation_config=output_config)\n", + "\n", + "config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful AI assistant.\"),\n", + " UserMessage(\"{{?text}}\"),\n", + " ]\n", + " ),\n", + " llm=LLM(\n", + " name=\"gpt-4o\",\n", + " ),\n", + " translation=translation_module\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78cb6487085e1a47", + "metadata": {}, + "outputs": [], + "source": [ + "result = orchestration_service.run(\n", + " config=config,\n", + " template_values=[\n", + " TemplateValue(name=\"text\", value=\"What is the capital of Germany?\")\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28acd2a79fd9e66b", + "metadata": {}, + "outputs": [], + "source": [ + "print(result.orchestration_result.choices[0].message.content)" + ] + }, + { + "cell_type": "markdown", + "id": "d4e27af8e7a0273a", + "metadata": {}, + "source": [ + "## Advanced Examples" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7476db3ffb49eb14", + "metadata": {}, + "outputs": [], + "source": [ + "service = OrchestrationService(api_url=YOUR_API_URL)" + ] + }, + { + "cell_type": "markdown", + "id": "10c1f142ece2d309", + "metadata": {}, + "source": [ + "### Translation Service\n", + "\n", + "This example extends the initial walkthrough of a basic orchestration pipeline by abstracting the translation task into its own reusable `TranslationService` class. Once the configuration is established, it can be easily adapted and reused for different translation scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72113e559b0875ad", + "metadata": {}, + "outputs": [], + "source": [ + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "\n", + "\n", + "class TranslationService:\n", + " def __init__(self, orchestration_service: OrchestrationService):\n", + " self.service = orchestration_service\n", + " self.config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful translation assistant.\"),\n", + " UserMessage(\n", + " \"Translate the following text to {{?to_lang}}: {{?text}}\"\n", + " ),\n", + " ],\n", + " defaults=[\n", + " TemplateValue(name=\"to_lang\", value=\"English\"),\n", + " ],\n", + " ),\n", + " llm=LLM(name=\"gpt-4o\"),\n", + " )\n", + "\n", + " def translate(self, text, to_lang):\n", + " response = self.service.run(\n", + " config=self.config,\n", + " template_values=[\n", + " TemplateValue(name=\"to_lang\", value=to_lang),\n", + " TemplateValue(name=\"text\", value=text),\n", + " ],\n", + " )\n", + "\n", + " return response.orchestration_result.choices[0].message.content\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb2e43db6665d162", + "metadata": {}, + "outputs": [], + "source": [ + "translator = TranslationService(orchestration_service=service)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b48f620b8ece05b1", + "metadata": {}, + "outputs": [], + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"French\")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c9cd07907bda369", + "metadata": {}, + "outputs": [], + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"Spanish\")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6eafbcb25180d359", + "metadata": {}, + "outputs": [], + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"German\")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "67bb6ee8f9221c0f", + "metadata": {}, + "source": [ + "### Chatbot with Memory\n", + "\n", + "This example demonstrates how to integrate the `OrchestrationService` with a chatbot to handle conversational flow.\n", + "\n", + "When making requests to the orchestration service, you can specify a list of messages as `history` that will be prepended to the templated content and processed by the templating module. These messages are plain, non-templated messages, as they typically represent past conversation outputs — such as in this chatbot scenario.\n", + "\n", + "It’s important to note that managing conversation history / state is handled locally in the `ChatBot` class, not by the orchestration service itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28a57d005dd6abfa", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List\n", + "\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "from gen_ai_hub.orchestration.models.message import Message, SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "\n", + "\n", + "class ChatBot:\n", + " def __init__(self, orchestration_service: OrchestrationService):\n", + " self.service = orchestration_service\n", + " self.config = OrchestrationConfig(\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"You are a helpful chatbot assistant.\"),\n", + " UserMessage(\"{{?user_query}}\"),\n", + " ],\n", + " ),\n", + " llm=LLM(name=\"gpt-4o\"),\n", + " )\n", + " self.history: List[Message] = []\n", + "\n", + " def chat(self, user_input):\n", + " response = self.service.run(\n", + " config=self.config,\n", + " template_values=[\n", + " TemplateValue(name=\"user_query\", value=user_input),\n", + " ],\n", + " history=self.history,\n", + " )\n", + "\n", + " message = response.orchestration_result.choices[0].message\n", + "\n", + " self.history = response.module_results.templating\n", + " self.history.append(message)\n", + "\n", + " return message.content\n", + "\n", + " def reset(self):\n", + " self.history = []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25bac947d3f51ba2", + "metadata": {}, + "outputs": [], + "source": [ + "bot = ChatBot(orchestration_service=service)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de06e9761b7af5f0", + "metadata": {}, + "outputs": [], + "source": [ + "print(bot.chat(\"Hello, how are you?\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d4b835cbd787bb4", + "metadata": {}, + "outputs": [], + "source": [ + "print(bot.chat(\"What's the weather like today?\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce80d406ccab9e68", + "metadata": {}, + "outputs": [], + "source": [ + "print(bot.chat(\"Can you remember what I first asked you?\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c18d7ed670c3506", + "metadata": {}, + "outputs": [], + "source": [ + "bot.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99a97a1ead5ca09", + "metadata": {}, + "outputs": [], + "source": [ + "print(bot.chat(\"Can you remember what I first asked you?\"))" + ] + }, + { + "cell_type": "markdown", + "id": "d0fee7d2dd8af5a4", + "metadata": {}, + "source": [ + "### Sentiment Analysis with Few Shot Learning \n", + "\n", + "This example demonstrates the different message `roles` in the templating module through a few-shot learning use case with the `FewShotLearner` class.\n", + "\n", + "- **Message Types:** Different message types (`SystemMessage`, `UserMessage`, `AssistantMessage`) structure the interaction and guide the model's behavior.\n", + "- **Templating:** The template includes these examples, ending with a `placeholder` ({{?user_input}}) for dynamic user input.\n", + "- **Few-Shot Examples:** Pairs of UserMessage and AssistantMessage show how the model should respond to similar queries.\n", + "\n", + "\n", + "The FewShotLearner class manages the dynamic creation of the template and ensures the correct message roles are used for each user input." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9296deee2b6a286e", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List, Tuple\n", + "\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "from gen_ai_hub.orchestration.models.message import (\n", + " SystemMessage,\n", + " UserMessage,\n", + " AssistantMessage,\n", + ")\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "\n", + "\n", + "class FewShotLearner:\n", + " def __init__(\n", + " self,\n", + " orchestration_service: OrchestrationService,\n", + " system_message: SystemMessage,\n", + " examples: List[Tuple[UserMessage, AssistantMessage]],\n", + " ):\n", + " self.service = orchestration_service\n", + " self.config = OrchestrationConfig(\n", + " template=self._create_few_shot_template(system_message, examples),\n", + " llm=LLM(name=\"gpt-4o-mini\"),\n", + " )\n", + "\n", + " @staticmethod\n", + " def _create_few_shot_template(\n", + " system_message: SystemMessage,\n", + " examples: List[Tuple[UserMessage, AssistantMessage]],\n", + " ) -> Template:\n", + " messages = [system_message]\n", + "\n", + " for example in examples:\n", + " messages.append(example[0])\n", + " messages.append(example[1])\n", + " messages.append(UserMessage(\"{{?user_input}}\"))\n", + "\n", + " return Template(messages=messages)\n", + "\n", + " def predict(self, user_input: str) -> str:\n", + " response = self.service.run(\n", + " config=self.config,\n", + " template_values=[TemplateValue(name=\"user_input\", value=user_input)],\n", + " )\n", + "\n", + " return response.orchestration_result.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1b054f8f344a862", + "metadata": {}, + "outputs": [], + "source": [ + "sentiment_examples = [\n", + " (UserMessage(\"I love this product!\"), AssistantMessage(\"Positive\")),\n", + " (UserMessage(\"This is terrible service.\"), AssistantMessage(\"Negative\")),\n", + " (UserMessage(\"The weather is okay today.\"), AssistantMessage(\"Neutral\")),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5babd104942009f", + "metadata": {}, + "outputs": [], + "source": [ + "sentiment_analyzer = FewShotLearner(\n", + " orchestration_service=service,\n", + " system_message=SystemMessage(\n", + " \"You are a sentiment analysis assistant. Classify the sentiment as Positive, Negative, or Neutral.\"\n", + " ),\n", + " examples=sentiment_examples,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f11c1b1bf9625782", + "metadata": {}, + "outputs": [], + "source": [ + "print(sentiment_analyzer.predict(\"The movie was a complete waste of time!\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52f3cde99c4b35f4", + "metadata": {}, + "outputs": [], + "source": [ + "print(\n", + " sentiment_analyzer.predict(\"The traffic was fortunately unusually light today.\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6556f4990073d78f", + "metadata": {}, + "outputs": [], + "source": [ + "print(\n", + " sentiment_analyzer.predict(\"I'm not sure how I feel about the recent events.\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4f94f52e", + "metadata": {}, + "source": [ + "## Async Support\n", + "\n", + "The `OrchestrationService` also supports asynchronous calls.\n", + "Use:\n", + "- `arun` from the async version of `run`\n", + "- `astream` from the async version of `stream`" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c446cdbf", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T18:52:35.911355Z", + "start_time": "2025-09-13T18:52:35.899990Z" + } + }, + "outputs": [], + "source": [ + "import asyncio\n", + "\n", + "from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage\n", + "from gen_ai_hub.orchestration.models.template import Template, TemplateValue\n", + "from gen_ai_hub.orchestration.models.llm import LLM\n", + "from gen_ai_hub.orchestration.models.config import OrchestrationConfig\n", + "\n", + "from IPython.display import display, Markdown # just for pretty print in jupyter\n", + "\n", + "\n", + "config = OrchestrationConfig(\n", + " llm=LLM(name=\"gemini-2.0-flash\"),\n", + " template=Template(\n", + " messages=[\n", + " SystemMessage(\"This is a system message.\"),\n", + " UserMessage(\"Write a markdown cheatsheet!\"),\n", + " ],\n", + " ),\n", + " )\n", + "\n", + "# Instantiate the orchestration service.\n", + "from gen_ai_hub.orchestration.service import OrchestrationService\n", + "orchestration_service = OrchestrationService(config=config)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8da1e87-3108-4464-87b2-b86aa69961f3", + "metadata": { + "ExecuteTime": { + "end_time": "2025-09-13T18:52:50.949638Z", + "start_time": "2025-09-13T18:52:38.448444Z" + } + }, + "outputs": [], + "source": [ + "async def test_async():\n", + " async_result = await orchestration_service.arun()\n", + " display(Markdown(async_result.orchestration_result.choices[0].message.content))\n", + "\n", + "await test_async()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fdb4a27-4520-43a6-9898-0066557ab830", + "metadata": {}, + "outputs": [], + "source": [ + "async def test_streaming_async():\n", + " streamed_content = \"\"\n", + " async for chunk in await orchestration_service.astream():\n", + " if chunk.orchestration_result.choices:\n", + " streamed_content += chunk.orchestration_result.choices[0].delta.content\n", + " display(Markdown(streamed_content), clear=True)\n", + "\n", + "await test_streaming_async()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/orchestration-service2.ipynb b/packages/gen/docs/gen_ai_hub/examples/orchestration-service2.ipynb new file mode 100644 index 0000000..6ade4d4 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/orchestration-service2.ipynb @@ -0,0 +1,2056 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration2)=\n", + "# Orchestration Service V2 API" + ], + "id": "d8b115a4d92fd4db" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This notebook demonstrates how to use the SDK to interact with the Orchestration Service V2, enabling the creation of AI-driven workflows by seamlessly integrating various modules, such as templating, large language models (LLMs), data masking and content filtering. By leveraging these modules, you can build complex, automated workflows that enhance the capabilities of your AI solutions. For more details on configuring and using these modules, please refer to the [Orchestration Service Documentation](https://help.sap.com/docs/ai-launchpad/sap-ai-launchpad/orchestration).", + "id": "25e1a6ae2c503530" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Prerequisite\n", + "\n", + "> **Important:** Before you begin using the SDK, make sure to set up a virtual deployment of the Orchestration Service.\n", + "\n", + "For detailed guidance on setting up the Orchestration Service, please refer to the setup guide [here](https://help.sap.com/docs/ai-launchpad/sap-ai-launchpad/create-deployment-for-orchestration)." + ], + "id": "ef9eb9a6a7b70d21" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Authentication\n", + "\n", + "By default, the `OrchestrationService` initializes a `GenAIHubProxyClient`, which automatically configures credentials using configuration files or environment variables, as outlined in the *Introduction* section.\n", + "\n", + "If you prefer to set credentials manually, you can provide a custom instance using the `proxy_client` parameter." + ], + "id": "5b5932e69ddd9d9b" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Basic Orchestration Pipeline\n", + "\n", + "Let's walk through a basic orchestration pipeline for a translation task." + ], + "id": "66a8b36fcc724b6c" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 1: Define the Template and Default Input Values\n", + "\n", + "The `Template` class is used to define structured message templates for generating dynamic interactions with language models. In this example, the template is designed for a translation assistant, allowing users to specify a language and text for translation." + ], + "id": "efa18d8a1bc94fa5" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:21:27.519791Z", + "start_time": "2026-03-19T07:21:26.902024Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import Template, SystemMessage, UserMessage\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful translation assistant.\"),\n", + " UserMessage(content=\"Translate the following text to {{?to_lang}}: {{?user_query}}\"),\n", + " ],\n", + " defaults={\"to_lang\": \"German\"}\n", + " )" + ], + "id": "68ae8781e1f37767", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "This template can be used to create translation requests where the language and text to be translated are specified dynamically. The placeholders in the `UserMessage` will be replaced with the actual values provided at runtime, and the default value for the language is set to German.", + "id": "8aa1bf41dd94da85" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 2: Define the LLM\n", + "\n", + "The `LLM` class is used to configure and initialize a language model for generating text based on specific parameters. In this example, we'll use the `gpt-4o` model to perform the translation task.\n", + "\n", + "**Note:** The Orchestration Service automatically manages the virtual deployment of the language model, so no additional setup is needed on your end." + ], + "id": "701646ebfe806c03" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:21:35.847306Z", + "start_time": "2026-03-19T07:21:35.845208Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import LLMModelDetails\n", + "\n", + "llm = LLMModelDetails(name=\"gpt-5-nano\", params={\"max_completion_tokens\": 512})" + ], + "id": "55c03da66cf6e8de", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Initializes the language model to use the `gpt-5-nano` model. It will generate responses up to 512 tokens in length.", + "id": "82ae689dce681b5f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 3: Create the Orchestration Configuration\n", + "\n", + "The `OrchestrationConfig` class defines a configuration for integrating various modules, such as templates and language models, into a cohesive orchestration setup. It specifies how these components interact and are configured to achieve the desired operational scenario." + ], + "id": "dc3f2a71cce77aa" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:21:52.276460Z", + "start_time": "2026-03-19T07:21:52.271917Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import PromptTemplatingModuleConfig, ModuleConfig, OrchestrationConfig\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)" + ], + "id": "62b4386a1a48c7b5", + "outputs": [], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 4: Run the Orchestration Request\n", + "\n", + "The `OrchestrationService` class is used to interact with a orchestration service instance by providing configuration details to initiate and manage its operations." + ], + "id": "10f94bed8b6ef93c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:22:00.455920Z", + "start_time": "2026-03-19T07:21:59.415073Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import OrchestrationService\n", + "\n", + "orchestration_service = OrchestrationService(config=config)" + ], + "id": "427b94b07e2ba67f", + "outputs": [], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Call the `run` method with the required `placeholder values`. The service will process the input according to the configuration and return the result.", + "id": "3151fa9176fb585d" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "result = orchestration_service.run(placeholder_values={\"user_query\": \"The Orchestration Service is working!\"})\n", + "print(result.final_result.choices[0].message.content)" + ], + "id": "3684a72a3856ca8f", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(prompt_registry)=\n", + "#### Referencing Templates in the Prompt Registry\n", + " In Step 3 you can also use a prompt template reference, which allows you to reuse existing templates stored in the Prompt Registry." + ], + "id": "1bd44dc91b57793d" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:22:21.413392Z", + "start_time": "2026-03-19T07:22:21.411795Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import TemplateRefByID, TemplateRefByScenarioNameVersion\n", + "\n", + "template_by_id = TemplateRefByID(id=\"648871d9-b207-441c-8c13-afee71b0dbec\") # this is just an example id\n", + "template_by_names = TemplateRefByScenarioNameVersion(scenario=\"translation\", name=\"translate_text\", version=\"0.1.0\")" + ], + "id": "118cf8c87dcfcf80", + "outputs": [], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(response_format)=\n", + "#### Overview of response_format Parameter Options\n", + "\n", + "The `response_format` parameter allows the model output to be formatted in several predefined ways, as follows:\n", + "\n", + "1. **text**: This is the simplest form where the model's output is generated as plain text. It is suitable for applications that require raw text processing.\n", + "\n", + "2. **json_object**: Under this setting, the model's output is structured as a JSON object. This is useful for applications that handle data in JSON format, enabling easy integration with web applications and APIs.\n", + "\n", + "3. **json_schema**: This setting allows the model's output to adhere to a defined JSON schema. This is particularly useful for applications that require strict data validation, ensuring the output matches a predefined schema." + ], + "id": "8af833056514e215" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:22:39.198453Z", + "start_time": "2026-03-19T07:22:39.196095Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import SystemMessage, UserMessage, Template, ResponseFormatText\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful translation assistant.\"),\n", + " UserMessage(content=\"{{?user_query}}\")\n", + " ],\n", + " response_format=ResponseFormatText(),\n", + " defaults={\"user_query\": \"Who was the first person on the moon?\"}\n", + ")\n", + "\n", + "# Response:\n", + "# The first man on the moon was Neil Armstrong." + ], + "id": "6344a28c59c46dbe", + "outputs": [], + "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:22:54.254255Z", + "start_time": "2026-03-19T07:22:54.246206Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import SystemMessage, UserMessage, Template, ResponseFormatJsonObject\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful translation assistant. Format the response as json.\"),\n", + " UserMessage(content=\"{{?user_query}}\")\n", + " ],\n", + " response_format=ResponseFormatJsonObject(),\n", + " defaults={\"user_query\": \"Who was the first person on the moon?\"}\n", + ")\n", + "\n", + "# Response:\n", + "# {\n", + "# \"First_man_on_the_moon\": \"Neil Armstrong\"\n", + "# }" + ], + "id": "b07c3c0b320e5026", + "outputs": [], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Important:** When using `response_format` as json_object, ensure that messages contain the word 'json' in some form.", + "id": "3bff15b49f9cbef7" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:23:14.596775Z", + "start_time": "2026-03-19T07:23:14.585103Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import SystemMessage, UserMessage, Template, ResponseFormatJsonSchema, JSONResponseSchema\n", + "\n", + "json_schema = {\n", + " \"title\": \"Person\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"firstName\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The person's first name.\"\n", + " },\n", + " \"lastName\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The person's last name.\"\n", + " }\n", + " }\n", + "}\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful translation assistant. Format the response as json.\"),\n", + " UserMessage(content=\"{{?user_query}}\")\n", + " ],\n", + " response_format=ResponseFormatJsonSchema(\n", + " json_schema=JSONResponseSchema(\n", + " name=\"person\", description=\"person mapping\", schema=json_schema\n", + " ),\n", + " ),\n", + " defaults={\"user_query\": \"Who was the first person on the moon?\"}\n", + ")\n", + "\n", + "# Response:\n", + "# {\n", + "# \"firstName\": \"Neil\",\n", + "# \"lastName\": \"Armstrong\"\n", + "# }" + ], + "id": "4779aa6b1627fd8c", + "outputs": [], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration_deployment)=\n", + "## Understanding Deployment Resolution\n", + "\n", + "The `OrchestrationService` class provides multiple ways to specify and target orchestration deployments when sending requests. Below are the available options:" + ], + "id": "af41bbe9b0279faf" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Default Behavior\n", + "\n", + "If no parameters are provided, the `OrchestrationService` automatically searches for a `RUNNING` deployment. If multiple running deployments exist, the service selects the most recently created one." + ], + "id": "9ae1192eb2ef5efd" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Direct Deployment Specification\n", + "\n", + "You can explicitly define the target deployment using the following options:\n", + "\n", + "1. **API URL** (`api_url`):\n", + " - Specify the exact URL assigned to the deployment during its creation.\n", + " - Refer to the Prerequisites section for more details on obtaining the deployment URL.\n", + "\n", + "2. **Deployment ID** (`deployment_id`):\n", + " - Use the unique identifier assigned to the deployment instead of the URL." + ], + "id": "66c869b15585442a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Config-Based Specification\n", + "\n", + "If you want to target deployments based on their configuration source, use one of the following options:\n", + "\n", + "1. **Configuration ID** (`config_id`):\n", + " - The `OrchestrationService` searches for a `RUNNING` deployment created using the provided configuration ID.\n", + "\n", + "2. **Configuration Name** (`config_name`):\n", + " - The service looks for a `RUNNING` deployment that matches the specified configuration name.\n", + "\n", + "If multiple deployments match the given configuration criteria, the most recently created one will be selected automatically." + ], + "id": "e65b02881577838d" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Optional Modules", + "id": "ee0538ff9e3de88f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Data Masking\n", + "\n", + "The `Data Masking` module `anonymizes` or `pseudonymizes` personally identifiable information (PII) before it is processed by the LLM module. Currently, `SAPDataPrivacyIntegration` is the only available masking provider.\n", + "\n", + "#### Masking Types\n", + "\n", + "- **Anonymization**: All identifying information is replaced with placeholders (e.g., MASKED_ENTITY), and the original data cannot be recovered, ensuring that no trace of the original information is retained.\n", + "- **Pseudonymization**: Data is substituted with unique placeholders (e.g., MASKED_ENTITY_ID), allowing the original information to be restored if needed.\n", + "\n", + "In both cases, the masking module identifies sensitive data and replaces it with appropriate placeholders before further processing.\n", + "\n", + "(allow_list)=\n", + "\n", + "#### Configuration Options\n", + "\n", + "- **entities**: Specify which types of entities to mask (e.g., EMAIL, PHONE, PERSON).\n", + "- **allowlist**: Provide specific terms or patterns that should be excluded from masking, even if they match entity types.\n", + "- **mask_grounding_input**: When enabled, ensures that masking is also applied to the context provided to the grounding module." + ], + "id": "a102e0350b4fd2b2" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:24:32.758004Z", + "start_time": "2026-03-19T07:24:28.674275Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2.utils import load_text_file\n", + "from gen_ai_hub.orchestration_v2 import (SystemMessage, UserMessage, Template, PromptTemplatingModuleConfig,\n", + " LLMModelDetails, ModuleConfig, OrchestrationConfig, OrchestrationService,\n", + " MaskingModuleConfig, MaskingProviderConfig, MaskingMethod, DPIStandardEntity,\n", + " ProfileEntity)\n", + "\n", + "orchestration_service = OrchestrationService()\n", + "\n", + "data_masking_config = MaskingModuleConfig(\n", + " providers=[MaskingProviderConfig(\n", + " method=MaskingMethod.ANONYMIZATION,\n", + " entities=[\n", + " DPIStandardEntity(type=ProfileEntity.ADDRESS),\n", + " DPIStandardEntity(type=ProfileEntity.EMAIL),\n", + " DPIStandardEntity(type=ProfileEntity.PHONE),\n", + " DPIStandardEntity(type=ProfileEntity.PERSON),\n", + " ],\n", + " allowlist=[\"M&K Group\"], # Terms to exclude from masking\n", + " )],\n", + "\n", + ")\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"Summarize the following CV in 10 sentences: {{?orgCV}}\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template, masking=data_masking_config)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "\n", + "cv_as_string = load_text_file(\"data/cv.txt\")\n", + "\n", + "result = orchestration_service.run(\n", + " config=config,\n", + " placeholder_values={\"orgCV\": cv_as_string}\n", + ")" + ], + "id": "e5f818a811909e83", + "outputs": [], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(result.final_result.choices[0].message.content)", + "id": "8d537f80a6cfe6e4" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(content_filtering)=\n", + "### Content Filtering\n", + "\n", + "The `Content Filtering` module can be configured to filter both the `input` to the LLM module (input filter) and the `output` generated by the LLM (output filter). The module uses predefined classification services to detect inappropriate or unwanted content. Azure Content Filter sensitivity is controlled by customizable `thresholds`, assuring the content aligns with the desired standards before processing or generating as output. Llama Guard 3 Filter, equipped with 14 categories, runs on a binary mechanism, accepting only true or false. Setting a category to true enables filtering for it. It's possible to execute both filters in a single request, optimizing efficiency." + ], + "id": "719b5e0b7627671b" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:25:18.924262Z", + "start_time": "2026-03-19T07:25:18.893833Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (AzureContentSafetyInput, AzureContentSafetyOutput, AzureThreshold,\n", + " LlamaGuard38bFilter, FilteringModuleConfig, InputFiltering, OutputFiltering,\n", + " AzureContentSafetyInputFilterConfig, AzureContentSafetyOutputFilterConfig,\n", + " LlamaGuard38bFilterConfig)\n", + "\n", + "content_filter_config = FilteringModuleConfig(\n", + " input=InputFiltering(filters=[\n", + " AzureContentSafetyInputFilterConfig(config=AzureContentSafetyInput(hate=AzureThreshold.ALLOW_SAFE,\n", + " violence=AzureThreshold.ALLOW_SAFE,\n", + " self_harm=AzureThreshold.ALLOW_SAFE,\n", + " sexual=AzureThreshold.ALLOW_SAFE)),\n", + " LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter(hate=True))\n", + " ]),\n", + " output=OutputFiltering(filters=[\n", + " AzureContentSafetyOutputFilterConfig(config=AzureContentSafetyOutput(hate=AzureThreshold.ALLOW_SAFE,\n", + " violence=AzureThreshold.ALLOW_SAFE,\n", + " self_harm=AzureThreshold.ALLOW_SAFE,\n", + " sexual=AzureThreshold.ALLOW_SAFE)),\n", + " LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter(hate=True))\n", + " ])\n", + "\n", + ")\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"{{?text}}\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template, filtering=content_filter_config)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "\n", + "client = OrchestrationService(config=config)" + ], + "id": "e5743fda48a03b", + "outputs": [], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import OrchestrationError\n", + "\n", + "try:\n", + " result = client.run(placeholder_values={\"text\": \"I hate you\"})\n", + " print(result.final_result.choices[0].message.content)\n", + "except OrchestrationError as er:\n", + " print(er.message)" + ], + "id": "46f631d9f77711f0", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration_streaming)=\n", + "## Streaming\n", + "\n", + "When you initiate an orchestration request, the full response is typically processed and delivered in one go. For longer responses, this can lead to delays in receiving the complete output. To mitigate this, you have the option to stream the results as they are being generated. This helps in rapidly processing or displaying initial portions of the results without waiting for the entire computation to finish.\n" + ], + "id": "b383c26e816ccfb6" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "To activate streaming, use the `stream` method of the `OrchestrationService` with the `stream` option in `OrchestrationConfig`. This method returns an object that streams chunks of the response as they become available. You can then extract relevant information from the `delta` field.\n", + "\n", + "Here's how you can set up a simple configuration to stream orchestration results:" + ], + "id": "2a1f96083b525bcc" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import GlobalStreamOptions\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"{{?text}}\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(\n", + " name=\"gpt-4o-mini\",\n", + " params={\n", + " \"max_completion_tokens\": 256,\n", + " \"temperature\": 0.0\n", + " }\n", + " )\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template)\n", + "\n", + "config = OrchestrationConfig(modules=module_config,\n", + " stream=GlobalStreamOptions(enabled=True))\n", + "\n", + "client = OrchestrationService(config=config)\n", + "\n", + "result = client.stream(placeholder_values={\n", + " \"text\": \"Which color is the sky? Answer in one sentence.\"\n", + "})\n", + "for part in result:\n", + " print(part.final_result.choices[0].delta.content)\n", + " print(\"*\" * 20)\n" + ], + "id": "7520e9502ae2e0ec", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Note:** As shown above, streaming responses contain a delta field instead of a message field.", + "id": "e010edf32f8f6b94" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "You can customize the global stream behavior by setting options like `chunk_size` which controls the amount of data processed in each chunk:", + "id": "dd2376220db6da05" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "config = OrchestrationConfig(modules=module_config,\n", + " stream=GlobalStreamOptions(enabled=True, chunk_size=25))\n", + "\n", + "client = OrchestrationService(config=config)\n", + "\n", + "result = client.stream(placeholder_values={\n", + " \"text\": \"Which color is the sky? Answer in one sentence.\"\n", + "})\n", + "for part in result:\n", + " print(part.final_result.choices[0].delta.content)\n", + " print(\"*\" * 20)" + ], + "id": "bc153c49faaf43e0" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Modules that influence or process streaming results, such as `OutputFiltering`, might need specific stream options. The `overlap` option allows you to include extra context during the filtering process:", + "id": "f9cc085d9148fa6a" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from gen_ai_hub.orchestration_v2 import FilteringStreamOptions\n", + "content_filter_config = FilteringModuleConfig(\n", + " output=OutputFiltering(\n", + " filters=[AzureContentSafetyOutputFilterConfig(config=AzureContentSafetyOutput(hate=0))],\n", + " stream_options=FilteringStreamOptions(overlap=20))\n", + ")\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"{{?text}}\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template, filtering=content_filter_config)\n", + "\n", + "config = OrchestrationConfig(modules=module_config,\n", + " stream=GlobalStreamOptions(enabled=True))\n", + "\n", + "client = OrchestrationService(config=config)\n", + "\n", + "response = client.stream(placeholder_values={\"text\": \"Which color is the sky? Answer in one sentence.\"})\n", + "\n", + "for chunk in response:\n", + " print(chunk.final_result.choices[0].delta.content, end='')\n" + ], + "id": "f9173122149a3e12" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(tool_calling)=\n", + "## Tool Calling (Function Calling)\n", + "\n", + "The Orchestration Service supports **tool calling**, which allows large language models (LLMs) to request the execution of external operations such as Python functions, API calls, or other tools as part of their workflow.\n", + "\n", + "This feature enables you to build advanced AI solutions that can perform calculations, access data, or interact with external systems in response to user input.\n", + "\n", + "---\n", + "\n", + "### Defining Tools\n", + "\n", + "You can define tools in several ways, depending on your requirements and the level of control you need.\n", + "\n", + "#### Using the Python Decorator\n", + "\n", + "The simplest way to define a tool is to decorate a Python function with `@function_tool()`. The function’s signature and docstring are used to describe the tool to the LLM." + ], + "id": "f5e6ca171dc54a97" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:26:07.354117Z", + "start_time": "2026-03-19T07:26:07.348749Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import function_tool\n", + "\n", + "@function_tool()\n", + "def multiply(a: int, b: int) -> int:\n", + " \"\"\"Multiply two numbers.\"\"\"\n", + " return a * b\n", + "\n", + "@function_tool()\n", + "def add(a: int, b: int) -> int:\n", + " \"\"\"Add two numbers.\"\"\"\n", + " return a + b\n", + "\n", + "tools = [multiply, add]" + ], + "id": "70b23e987fda41f1", + "outputs": [], + "execution_count": 14 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### Using the `FunctionTool` Class\n", + "\n", + "For more control, you can use the `FunctionTool` class directly. This is useful if you want to customize the schema, enable strict argument checking, or wrap an existing function." + ], + "id": "e558dbca183e48a7" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:26:14.152448Z", + "start_time": "2026-03-19T07:26:14.150221Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import FunctionTool, FunctionObject\n", + "\n", + "def get_weather(location: str) -> str:\n", + " \"\"\"Get current temperature for a given location.\"\"\"\n", + " # Replace with your actual implementation\n", + " return \"22°C\"\n", + "\n", + "weather_tool_func = FunctionObject(\n", + " name=\"get_weather\",\n", + " description=\"Get current temperature for a given location.\",\n", + " parameters={\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City and country e.g. BogotÃĄ, Colombia\"\n", + " }\n", + " },\n", + " \"required\": [\"location\"],\n", + " \"additionalProperties\": False\n", + " },\n", + " strict=True,\n", + " function=get_weather\n", + ")\n", + "\n", + "weather_tool = FunctionTool(function=weather_tool_func)\n", + "\n", + "tools = [weather_tool]" + ], + "id": "14f259a50d7a856c", + "outputs": [], + "execution_count": 15 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "You can also create a `FunctionTool` from a function using the `from_function` static method:", + "id": "b86d28307b869444" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "weather_tool = FunctionTool.from_function(get_weather, strict=True)\n", + "tools = [weather_tool]" + ], + "id": "757ae1af213fedd8" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### Using a JSON Schema Dictionary\n", + "\n", + "You can define a tool directly as a JSON schema dictionary. This is useful if you want to specify the tool interface without implementing the function in Python, or if you want to integrate with external systems." + ], + "id": "85dfc74ee8420281" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "tools = [{\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_weather\",\n", + " \"description\": \"Get current temperature for a given location.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"City and country e.g. BogotÃĄ, Colombia\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"location\"\n", + " ],\n", + " \"additionalProperties\": False\n", + " },\n", + " \"strict\": True\n", + " }\n", + "}]" + ], + "id": "a2bd6ac0ce9d7e5c" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "You can then attach any of these tool definitions to your template:", + "id": "5d85e6155ac2959b" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:26:32.640345Z", + "start_time": "2026-03-19T07:26:32.636307Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import Template, SystemMessage, UserMessage\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a weather assistant.\"),\n", + " UserMessage(content=\"What is the temperature in {{?location}}?\"),\n", + " ],\n", + " tools=tools,\n", + ")" + ], + "id": "23d3a1974c34c1a", + "outputs": [], + "execution_count": 16 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Synchronous Tool Call Workflow\n", + "\n", + "When the LLM decides to call a tool, the orchestration response will include a `tool_calls` field. You are responsible for executing the tool(s), adding the results to the conversation history, and running the orchestration again to get the final answer." + ], + "id": "59d9ddcf139f2e34" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from typing import List\n", + "from gen_ai_hub.orchestration_v2 import ChatMessage, SystemMessage, UserMessage, ToolChatMessage\n", + "\n", + "\n", + "# Assume 'template' and 'weather_tool' are defined as above\n", + "llm = LLMModelDetails(name=\"gpt-4o-mini\", params={\"max_completion_tokens\": 200, \"temperature\": 0.0})\n", + "rompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "module_config = ModuleConfig(prompt_templating=prompt_template)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "\n", + "client = OrchestrationService(config=config)\n", + "template_values = {\"location\": \"BogotÃĄ, Colombia\"}\n", + "\n", + "# First run: triggers tool call\n", + "service = OrchestrationService()\n", + "response = service.run(config=config, placeholder_values=template_values)\n", + "tool_calls = response.final_result.choices[0].message.tool_calls\n", + "\n", + "# Execute tool(s) and build new history\n", + "history: List[ChatMessage] = []\n", + "history.extend(response.intermediate_results.templating)\n", + "history.append(response.final_result.choices[0].message)\n", + "\n", + "for tool_call in tool_calls:\n", + " # For FunctionTool, use .execute(**tool_call.function.parse_arguments())\n", + " result = weather_tool.execute(**tool_call.function.parse_arguments())\n", + " tool_message = ToolChatMessage(\n", + " content=str(result),\n", + " tool_call_id=tool_call.id,\n", + " )\n", + " history.append(tool_message)\n", + "\n", + "# Second run: LLM receives tool result and produces final answer\n", + "response2 = service.run(\n", + " config=config,\n", + " placeholder_values=template_values,\n", + " history=history,\n", + ")\n", + "print(response2.final_result.choices[0].message.content)" + ], + "id": "b8f342d04ef20f2b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Streaming Tool Calls\n", + "\n", + "When using streaming, tool calls may be split across multiple chunks. The `delta.tool_calls` field in each chunk contains partial or complete tool call information. You may need to buffer and concatenate arguments if they arrive in pieces." + ], + "id": "721d327f7e5615ed" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# Assume 'config' and 'service' are defined as above\n", + "config = OrchestrationConfig(modules=module_config, stream=GlobalStreamOptions(enabled=True))\n", + "service = OrchestrationService()\n", + "stream = service.stream(config=config, placeholder_values=template_values)\n", + "\n", + "final_tool_calls = {}\n", + "\n", + "for chunk in stream:\n", + " for tool_call in chunk.final_result.choices[0].delta.tool_calls or []:\n", + " index = tool_call.index\n", + " if index not in final_tool_calls:\n", + " final_tool_calls[index] = tool_call\n", + " else:\n", + " # Concatenate arguments if split across chunks\n", + " final_tool_calls[index].function.arguments += tool_call.function.arguments\n", + "\n", + "# Now final_tool_calls contains all tool calls with complete arguments" + ], + "id": "ea2a364927c0dd41" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "**âš ī¸ Note on Agentic Loop Support:**\n", + "\n", + "> The current SDK **does not provide built-in abstractions or convenience methods for managing the agentic loop** (the process of automatically handling tool call detection, execution, and iterative orchestration until a final answer is produced).\n", + ">\n", + "> As a user, you are responsible for:\n", + "> - Detecting tool calls in the LLM response\n", + "> - Executing the corresponding Python functions\n", + "> - Appending tool results to the conversation history (as `ToolMessage`)\n", + "> - Re-invoking the orchestration service as needed\n", + ">\n", + "> This approach gives you maximum flexibility, but you must implement the orchestration loop logic yourself." + ], + "id": "9fed1b18ba749dff" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(input_images)=\n", + "## Using Images as Input\n", + "\n", + "The Orchestration Service supports multimodal prompts, enabling you to include images alongside text in your messages. This powerful feature unlocks a variety of applications, such as visual question answering (VQA), image captioning, object recognition, and generating text creatively based on visual input.\n", + "\n", + "This guide details how to prepare image inputs, integrate them into your prompts, and execute the orchestration to get insightful responses.\n", + "\n", + "### 1. Preparing Image Inputs\n", + "\n", + "To use an image, you first need to represent it as an `ImageItem` object. The `gen_ai_hub.orchestration.models.multimodal_items.ImageItem` class provides two convenient ways to do this:\n", + "\n", + "#### a) From a URL or Data URL\n", + "\n", + "This method is ideal for images hosted online or when you have the image data encoded as a Data URL (base64 encoded).\n", + "\n", + "* **Standard URL:** Provide a direct web link to the image file.\n", + "* **Data URL:** Provide the image data directly embedded in the URL string.\n", + "\n", + "**Note:** For web URLs, ensure the image is publicly accessible, as the service will need to fetch it." + ], + "id": "c34749a2a7157b19" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:27:20.299676Z", + "start_time": "2026-03-19T07:27:20.296308Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import ImageItem\n", + "\n", + "# Example 1: Image from a standard, publicly accessible URL\n", + "# Ensure the URL points directly to the image file (e.g., .png, .jpg, ...)\n", + "image_from_web = ImageItem(url=\"https://picsum.photos/id/1/200/300\") # example image URL\n", + "\n", + "# Example 2: Image from a Data URL (base64-encoded)\n", + "# This is useful when you have the image content as a string.\n", + "# The format is \"data:[][;base64],\"\n", + "image_from_data_url = ImageItem(\n", + " url=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==\"\n", + ")" + ], + "id": "f6c9ee8bbe87618e", + "outputs": [], + "execution_count": 18 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### b) From a Local File\n", + "\n", + "If your image resides on your local filesystem, you can load it directly using the `ImageItem.from_file()` class method.\n", + "The `from_file` method handles opening, reading, and base64 encoding the image data for you, packaging it into an `ImageItem`." + ], + "id": "a1409812f52d7ef2" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "from gen_ai_hub.orchestration_v2 import ImageItem\n", + "\n", + "# Example: Image from a local file path\n", + "# To use this 'image_from_local_file' object, ensure it was successfully created.\n", + "try:\n", + " image_from_local_file = ImageItem.from_file(\"path/to/your/local/image.jpeg\")\n", + "except FileNotFoundError:\n", + " print(\"Error: The specified image file was not found.\")\n", + "except Exception as e:\n", + " print(f\"An error occurred while loading the image: {e}\")" + ], + "id": "2abef5e8ef2ccf60" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### 2. Adding Images to a Prompt\n", + "\n", + "Once you have your `ImageItem` object(s), you can combine them with text to create a multimodal prompt. This is done by passing a list containing `ImageItem` instances and text strings to the `content` parameter of a `UserMessage`." + ], + "id": "78f150ec68206f25" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import UserMessage\n", + "\n", + "llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + "# Simple visual question answering\n", + "content_vqa = [image_from_web, \"What objects are prominent in this image?\"]\n", + "\n", + "# Create a UserMessage with the mixed content\n", + "user_message = UserMessage(content=content_vqa)\n", + "\n", + "# Create a Template containing the UserMessage\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "service = OrchestrationService(config=config)\n", + "response = service.run()\n", + "print(response.final_result.choices[0].message.content)" + ], + "id": "595c6615905e4519", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(translation)=\n", + "## Translation\n", + "Translation module can be used to translate text from one language to another. You can use this module to translate input text before it is processed by the LLM module, or to translate the output generated by the LLM module. The translation module uses the SAP Document Translation service to perform the translation." + ], + "id": "e00e2ddabfa857d0" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:28:43.144994Z", + "start_time": "2026-03-19T07:28:43.113534Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (SystemMessage, UserMessage, Template, PromptTemplatingModuleConfig,\n", + " LLMModelDetails, ModuleConfig, OrchestrationConfig, OrchestrationService,\n", + " TranslationModuleConfig, SAPDocumentTranslationInput,\n", + " SAPDocumentTranslationOutput, InputTranslationConfig, OutputTranslationConfig)\n", + "\n", + "translation_config = TranslationModuleConfig(\n", + " input=SAPDocumentTranslationInput(\n", + " config=InputTranslationConfig(\n", + " source_language=\"en-US\",\n", + " target_language=\"de-DE\"\n", + " )\n", + " ),\n", + " output=SAPDocumentTranslationOutput(\n", + " config=OutputTranslationConfig(\n", + " source_language=\"de-DE\",\n", + " target_language=\"fr-FR\"\n", + " )\n", + " )\n", + ")\n", + "\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"{{?text}}\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template, translation=translation_config)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "\n", + "orchestration_service = OrchestrationService()" + ], + "id": "249efe8e5e9cc60f", + "outputs": [], + "execution_count": 20 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "result = orchestration_service.run(\n", + " config=config,\n", + " placeholder_values={\"text\": \"What is the capital of Germany?\"}\n", + ")" + ], + "id": "fb96a0b39c73e145" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(result.final_result.choices[0].message.content)", + "id": "9faa54034fc077a" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Advanced Examples", + "id": "5aec4fd632c3a0fd" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "service = OrchestrationService(api_url=YOUR_API_URL)", + "id": "d2a6a1c7f7e3aad3" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Translation Service\n", + "\n", + "This example extends the initial walkthrough of a basic orchestration pipeline by abstracting the translation task into its own reusable `TranslationService` class. Once the configuration is established, it can be easily adapted and reused for different translation scenarios." + ], + "id": "d05456d9685578c6" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:29:31.451027Z", + "start_time": "2026-03-19T07:29:31.438376Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (OrchestrationConfig, ModuleConfig, LLMModelDetails, SystemMessage, UserMessage,\n", + " Template, PromptTemplatingModuleConfig, OrchestrationService)\n", + "\n", + "\n", + "class TranslationService:\n", + " def __init__(self, orchestration_service: OrchestrationService):\n", + " self.service = orchestration_service\n", + " self.template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful AI assistant.\"),\n", + " UserMessage(content=\"Translate the following text to {{?to_lang}}: {{?text}}\"),\n", + " ],\n", + " defaults={\"to_lang\": \"en-US\"}\n", + " )\n", + "\n", + " self.llm=LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + " self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm)\n", + "\n", + " self.module_config = ModuleConfig(prompt_templating=self.prompt_template)\n", + "\n", + " self.config = OrchestrationConfig(modules=self.module_config)\n", + "\n", + " def translate(self, text, to_lang):\n", + " response = self.service.run(\n", + " config=self.config,\n", + " placeholder_values={\n", + " \"to_lang\": to_lang,\n", + " \"text\": text\n", + " },\n", + " )\n", + "\n", + " return response.final_result.choices[0].message.content\n" + ], + "id": "f0c675fc9f10b823", + "outputs": [], + "execution_count": 21 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "translator = TranslationService(orchestration_service=service)", + "id": "4b1507b5111848dc" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"French\")\n", + "print(result)" + ], + "id": "15bf0d86f3404076" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"Spanish\")\n", + "print(result)" + ], + "id": "9134210a296d1a60" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "result = translator.translate(text=\"Hello, world!\", to_lang=\"German\")\n", + "print(result)" + ], + "id": "53f4af1db029400" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Chatbot with Memory\n", + "\n", + "This example demonstrates how to integrate the `OrchestrationService` with a chatbot to handle conversational flow.\n", + "\n", + "When making requests to the orchestration service, you can specify a list of messages as `history` that will be prepended to the templated content and processed by the templating module. These messages are plain, non-templated messages, as they typically represent past conversation outputs — such as in this chatbot scenario.\n", + "\n", + "It’s important to note that managing conversation history / state is handled locally in the `ChatBot` class, not by the orchestration service itself." + ], + "id": "1a31cbd3f644327d" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:30:01.880952Z", + "start_time": "2026-03-19T07:30:01.874120Z" + } + }, + "cell_type": "code", + "source": [ + "from typing import List\n", + "\n", + "from gen_ai_hub.orchestration_v2 import (OrchestrationConfig, ModuleConfig, LLMModelDetails, ChatMessage, SystemMessage,\n", + " UserMessage, Template, PromptTemplatingModuleConfig, OrchestrationService)\n", + "\n", + "\n", + "class ChatBot:\n", + " def __init__(self, orchestration_service: OrchestrationService):\n", + " self.service = orchestration_service\n", + " self.template = Template(\n", + " template=[\n", + " SystemMessage(content=\"You are a helpful chatbot assistant.\"),\n", + " UserMessage(content=\"{{?user_query}}\")\n", + " ]\n", + " )\n", + "\n", + " self.llm = LLMModelDetails(name=\"gpt-4o\")\n", + "\n", + " self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm)\n", + "\n", + " self.module_config = ModuleConfig(prompt_templating=self.prompt_template)\n", + "\n", + " self.config = OrchestrationConfig(modules=self.module_config)\n", + "\n", + " self.history: List[ChatMessage] = []\n", + "\n", + " def chat(self, user_input):\n", + " response = self.service.run(\n", + " config=self.config,\n", + " placeholder_values={\"user_query\": user_input},\n", + " history=self.history,\n", + " )\n", + "\n", + " message = response.final_result.choices[0].message\n", + "\n", + " self.history = response.intermediate_results.templating\n", + " self.history.append(message)\n", + "\n", + " return message.content\n", + "\n", + " def reset(self):\n", + " self.history = []" + ], + "id": "797481251e803a8", + "outputs": [], + "execution_count": 22 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "bot = ChatBot(orchestration_service=OrchestrationService())", + "id": "2203c5a8987476d0" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(bot.chat(\"Hello, how are you?\"))", + "id": "e6a9a551086be501" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(bot.chat(\"What's the weather like today?\"))", + "id": "55b89a379f235062" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(bot.chat(\"Can you remember what I first asked you?\"))", + "id": "1e0cbe9d7029aa5b" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "bot.reset()", + "id": "eebcf6592d2f4342" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(bot.chat(\"Can you remember what I first asked you?\"))", + "id": "adb08617bb2073b3" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Sentiment Analysis with Few Shot Learning \n", + "\n", + "This example demonstrates the different message `roles` in the templating module through a few-shot learning use case with the `FewShotLearner` class.\n", + "\n", + "- **Message Types:** Different message types (`SystemMessage`, `UserMessage`, `AssistantMessage`) structure the interaction and guide the model's behavior.\n", + "- **Templating:** The template includes these examples, ending with a `placeholder` ({{?user_input}}) for dynamic user input.\n", + "- **Few-Shot Examples:** Pairs of UserMessage and AssistantMessage show how the model should respond to similar queries.\n", + "\n", + "\n", + "The FewShotLearner class manages the dynamic creation of the template and ensures the correct message roles are used for each user input." + ], + "id": "37bcd78e032fbae" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:31:13.521272Z", + "start_time": "2026-03-19T07:31:13.515516Z" + } + }, + "cell_type": "code", + "source": [ + "from typing import List, Tuple\n", + "\n", + "from gen_ai_hub.orchestration_v2 import (OrchestrationConfig,ModuleConfig, LLMModelDetails, SystemMessage, UserMessage,\n", + " AssistantMessage, Template, PromptTemplatingModuleConfig, OrchestrationService)\n", + "\n", + "\n", + "class FewShotLearner:\n", + " def __init__(\n", + " self,\n", + " orchestration_service: OrchestrationService,\n", + " system_message: SystemMessage,\n", + " examples: List[Tuple[UserMessage, AssistantMessage]],\n", + " ):\n", + " self.service = orchestration_service\n", + "\n", + "\n", + " self.llm = LLMModelDetails(name=\"gpt-4o-mini\")\n", + "\n", + " self.prompt_template = PromptTemplatingModuleConfig(\n", + " prompt=self._create_few_shot_template(system_message, examples),\n", + " model=self.llm\n", + " )\n", + "\n", + " self.module_config = ModuleConfig(prompt_templating=self.prompt_template)\n", + "\n", + " self.config = OrchestrationConfig(modules=self.module_config)\n", + "\n", + " @staticmethod\n", + " def _create_few_shot_template(\n", + " system_message: SystemMessage,\n", + " examples: List[Tuple[UserMessage, AssistantMessage]],\n", + " ) -> Template:\n", + " messages = [system_message]\n", + "\n", + " for example in examples:\n", + " messages.append(example[0])\n", + " messages.append(example[1])\n", + " messages.append(UserMessage(content=\"{{?user_input}}\"))\n", + "\n", + " return Template(template=messages)\n", + "\n", + " def predict(self, user_input: str) -> str:\n", + " response = self.service.run(\n", + " config=self.config,\n", + " placeholder_values={\"user_input\": user_input},\n", + " )\n", + "\n", + " return response.final_result.choices[0].message.content" + ], + "id": "87097426ad52e2c2", + "outputs": [], + "execution_count": 23 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "sentiment_examples = [\n", + " (UserMessage(content=\"I love this product!\"), AssistantMessage(content=\"Positive\")),\n", + " (UserMessage(content=\"This is terrible service.\"), AssistantMessage(content=\"Negative\")),\n", + " (UserMessage(content=\"The weather is okay today.\"), AssistantMessage(content=\"Neutral\")),\n", + "]" + ], + "id": "13cc0919eb1232e0" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "sentiment_analyzer = FewShotLearner(\n", + " orchestration_service=OrchestrationService(),\n", + " system_message=SystemMessage(\n", + " content=\"You are a sentiment analysis assistant. Classify the sentiment as Positive, Negative, or Neutral.\"\n", + " ),\n", + " examples=sentiment_examples,\n", + ")" + ], + "id": "d04872dcb560e15c" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "print(sentiment_analyzer.predict(\"The movie was a complete waste of time!\"))", + "id": "75b35fb528b2f318" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "print(\n", + " sentiment_analyzer.predict(\"The traffic was fortunately unusually light today.\")\n", + ")" + ], + "id": "33eeea427b8937f0" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "print(\n", + " sentiment_analyzer.predict(\"I'm not sure how I feel about the recent events.\")\n", + ")" + ], + "id": "65b86a4daa668515" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration_async)=\n", + "## Async Support\n", + "\n", + "The `OrchestrationService` also supports asynchronous calls.\n", + "Use:\n", + "- `arun` from the async version of `run`\n", + "- `astream` from the async version of `stream`" + ], + "id": "51dfba0273406622" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:31:49.927838Z", + "start_time": "2026-03-19T07:31:49.898436Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (SystemMessage, UserMessage, Template, PromptTemplatingModuleConfig,\n", + " LLMModelDetails, OrchestrationConfig, ModuleConfig)\n", + "\n", + "from IPython.display import display, Markdown # just for pretty print in jupyter\n", + "\n", + "template = Template(\n", + " template=[\n", + " SystemMessage(content=\"This is a system message.\"),\n", + " UserMessage(content=\"Write a markdown cheatsheet!\"),\n", + " ]\n", + " )\n", + "\n", + "llm=LLMModelDetails(name=\"gemini-2.0-flash\")\n", + "\n", + "prompt_template = PromptTemplatingModuleConfig(prompt=template,\n", + " model=llm)\n", + "\n", + "module_config = ModuleConfig(prompt_templating=prompt_template)\n", + "\n", + "config = OrchestrationConfig(modules=module_config)\n", + "\n", + "# Instantiate the orchestration service.\n", + "from gen_ai_hub.orchestration_v2 import OrchestrationService\n", + "orchestration_service = OrchestrationService(config=config)\n" + ], + "id": "c8b1be600f3eff58", + "outputs": [], + "execution_count": 24 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "async def test_async():\n", + " async_result = await orchestration_service.arun()\n", + " display(Markdown(async_result.final_result.choices[0].message.content))\n", + "\n", + "await test_async()" + ], + "id": "669448831d08f18" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import GlobalStreamOptions\n", + "\n", + "config_stream = OrchestrationConfig(modules=module_config,stream=GlobalStreamOptions(enabled=True))\n", + "\n", + "async def test_streaming_async():\n", + " streamed_content = \"\"\n", + " async for chunk in await orchestration_service.astream(config=config_stream):\n", + " streamed_content += chunk.final_result.choices[0].delta.content\n", + " display(Markdown(streamed_content))\n", + "\n", + "await test_streaming_async()" + ], + "id": "101d32c4d15c28fc", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration_embeddings)=\n", + "## Embeddings\n", + "\n", + "The Orchestration Service provides an embeddings endpoint for generating vector representations of text. Embeddings capture the semantic meaning of text, enabling powerful applications like semantic search, document clustering, and retrieval-augmented generation (RAG).\n", + "\n", + "**Key Use Cases:**\n", + "- **Semantic Search**: Find documents based on meaning, not just keywords\n", + "- **RAG (Retrieval-Augmented Generation)**: Retrieve relevant context for LLM prompts\n", + "- **Document Clustering**: Group similar documents together\n", + "- **Similarity Comparison**: Measure how semantically similar two texts are" + ], + "id": "8f64408b86b9aac7" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Basic Usage\n", + "\n", + "Generate an embedding for a single text string with minimal configuration." + ], + "id": "eed7aac13f9af34b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (OrchestrationService, EmbeddingsOrchestrationConfig, EmbeddingsModuleConfigs,\n", + " EmbeddingsModelConfig, EmbeddingsModelDetails, EmbeddingsInput)\n", + "\n", + "service = OrchestrationService()\n", + "\n", + "# Minimal configuration - just specify the model\n", + "embeddings_config = EmbeddingsOrchestrationConfig(\n", + " modules=EmbeddingsModuleConfigs(\n", + " embeddings=EmbeddingsModelConfig(\n", + " model=EmbeddingsModelDetails(name=\"text-embedding-3-large\")\n", + " )\n", + " )\n", + ")\n", + "\n", + "response = service.embed(\n", + " config=embeddings_config,\n", + " input=EmbeddingsInput(text=\"Hello World!\")\n", + ")\n", + "\n", + "embedding = response.final_result.data[0].embedding\n", + "print(f\"Embedding dimensions: {len(embedding)}\")\n", + "print(f\"First 5 values: {embedding[:5]}\")" + ], + "id": "14d32266c44b3d41", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Customizing Embedding Parameters\n", + "\n", + "You can customize the embedding output with parameters like `dimensions`, `encoding_format`, and `normalize`.\n", + "\n", + "| Parameter | Description | Values |\n", + "|-----------|-------------|-----------------------------|\n", + "| `dimensions` | Number of dimensions in the output | e.g. 256, 512, 1536, 3072 |\n", + "| `encoding_format` | Output format | `FLOAT`, `BASE64`, `BINARY` |\n", + "| `normalize` | Normalize the vector | `True`, `False` |" + ], + "id": "6c2e0fec7a1bfb4d" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import EmbeddingsModelParams, EmbeddingsEncodingFormat\n", + "\n", + "embeddings_config_custom = EmbeddingsOrchestrationConfig(\n", + " modules=EmbeddingsModuleConfigs(\n", + " embeddings=EmbeddingsModelConfig(\n", + " model=EmbeddingsModelDetails(\n", + " name=\"text-embedding-3-large\",\n", + " params=EmbeddingsModelParams(\n", + " dimensions=256, # Reduce dimensions for efficiency\n", + " encoding_format=EmbeddingsEncodingFormat.FLOAT,\n", + " normalize=True\n", + " )\n", + " )\n", + " )\n", + " )\n", + ")\n", + "\n", + "response = service.embed(\n", + " config=embeddings_config_custom,\n", + " input=EmbeddingsInput(text=\"Hello World!\")\n", + ")\n", + "\n", + "print(f\"Embedding dimensions: {len(response.final_result.data[0].embedding)}\")" + ], + "id": "18ec9508f808ca86", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Batch Embeddings\n", + "\n", + "Generate embeddings for multiple texts in a single request for better efficiency." + ], + "id": "304bfc990c316b5f" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "documents = [\n", + " \"Artificial intelligence is transforming industries worldwide.\",\n", + " \"Machine learning models require large amounts of training data.\",\n", + " \"Neural networks are inspired by the human brain structure.\",\n", + " \"Deep learning has achieved breakthroughs in image recognition.\"\n", + "]\n", + "\n", + "response = service.embed(\n", + " config=embeddings_config,\n", + " input=EmbeddingsInput(text=documents)\n", + ")\n", + "\n", + "print(f\"Generated {len(response.final_result.data)} embeddings\")\n", + "for result in response.final_result.data:\n", + " print(f\" Index {result.index}: {len(result.embedding)} dimensions\")" + ], + "id": "a3fbcc6bb228c9b6" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Input Type Hints (Asymmetric Search)\n", + "\n", + "Some embedding models support asymmetric search, where queries and documents are embedded differently for better retrieval. Use the `type` parameter to hint the purpose of your text.\n", + "\n", + "| Type | Use Case |\n", + "|------|----------|\n", + "| `TEXT` | General purpose (default) |\n", + "| `DOCUMENT` | Content to be indexed and searched |\n", + "| `QUERY` | Search queries to find relevant documents |" + ], + "id": "77ce13be4c12e88c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:36:14.961516Z", + "start_time": "2026-03-19T07:36:14.113464Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import EmbeddingsInputType\n", + "\n", + "# Embed a document for storage in a vector database\n", + "doc_response = service.embed(\n", + " config=embeddings_config,\n", + " input=EmbeddingsInput(\n", + " text=\"SAP is a German multinational software company that develops enterprise software.\",\n", + " type=EmbeddingsInputType.DOCUMENT\n", + " )\n", + ")\n", + "\n", + "# Embed a query for searching\n", + "query_response = service.embed(\n", + " config=embeddings_config,\n", + " input=EmbeddingsInput(\n", + " text=\"What is SAP?\",\n", + " type=EmbeddingsInputType.QUERY\n", + " )\n", + ")" + ], + "id": "16713afef9aaf491", + "outputs": [], + "execution_count": 29 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Embeddings with Data Masking\n", + "\n", + "When embedding sensitive data, use the data masking module to anonymize PII before generating embeddings. This ensures sensitive information is not exposed to the embedding model." + ], + "id": "be66c74b54173af4" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (MaskingModuleConfig, MaskingMethod, MaskingProviderConfig, DPIStandardEntity,\n", + " ProfileEntity)\n", + "\n", + "embeddings_config_with_masking = EmbeddingsOrchestrationConfig(\n", + " modules=EmbeddingsModuleConfigs(\n", + " embeddings=EmbeddingsModelConfig(\n", + " model=EmbeddingsModelDetails(name=\"text-embedding-3-large\")\n", + " ),\n", + " masking=MaskingModuleConfig(\n", + " masking_providers=[\n", + " MaskingProviderConfig(\n", + " method=MaskingMethod.ANONYMIZATION,\n", + " entities=[\n", + " DPIStandardEntity(type=ProfileEntity.PERSON),\n", + " DPIStandardEntity(type=ProfileEntity.EMAIL),\n", + " DPIStandardEntity(type=ProfileEntity.PHONE),\n", + " ]\n", + " )\n", + " ]\n", + " )\n", + " )\n", + ")\n", + "\n", + "response = service.embed(\n", + " config=embeddings_config_with_masking,\n", + " input=EmbeddingsInput(\n", + " text=\"Contact John Smith at john.smith@example.com or call 555-123-4567.\"\n", + " )\n", + ")\n", + "\n", + "print(f\"Embedding generated with PII masked\")\n", + "print(f\"Intermediate results: {response.intermediate_results}\")\n", + "print(f\"Dimensions: {len(response.final_result.data[0].embedding)}\")" + ], + "id": "466214c78e1cac56", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "#### Masking with Custom Entities and Allowlist\n", + "\n", + "Use regular expressions to mask custom patterns and allowlists to exclude specific terms from masking." + ], + "id": "8b862c98f3f31303" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-19T07:36:55.310595Z", + "start_time": "2026-03-19T07:36:53.720489Z" + } + }, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import DPICustomEntity, DPIMethodConstant\n", + "\n", + "embeddings_config_advanced_masking = EmbeddingsOrchestrationConfig(\n", + " modules=EmbeddingsModuleConfigs(\n", + " embeddings=EmbeddingsModelConfig(\n", + " model=EmbeddingsModelDetails(name=\"text-embedding-3-large\")\n", + " ),\n", + " masking=MaskingModuleConfig(\n", + " masking_providers=[\n", + " MaskingProviderConfig(\n", + " method=MaskingMethod.ANONYMIZATION,\n", + " entities=[\n", + " DPIStandardEntity(type=ProfileEntity.PERSON),\n", + " DPIStandardEntity(type=ProfileEntity.ORG),\n", + " # Custom pattern for internal IDs like \"89-SAP-550\"\n", + " DPICustomEntity(\n", + " regex=r\"\\b[0-9]{2}-SAP-[0-9]{3}\\b\",\n", + " replacement_strategy=DPIMethodConstant(\n", + " method=\"constant\",\n", + " value=\"REDACTED_ID\"\n", + " )\n", + " ),\n", + " ],\n", + " # These terms will NOT be masked\n", + " allowlist=[\"SAP\", \"Microsoft\"]\n", + " )\n", + " ]\n", + " )\n", + " )\n", + ")\n", + "\n", + "response = service.embed(\n", + " config=embeddings_config_advanced_masking,\n", + " input=EmbeddingsInput(\n", + " text=\"Employee John Doe (ID: 89-SAP-550) works at SAP with Microsoft partners.\"\n", + " )\n", + ")" + ], + "id": "8f559491b4487002", + "outputs": [], + "execution_count": 31 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Async Embeddings\n", + "\n", + "For non-blocking operations, use the async `aembed` method." + ], + "id": "dea5a8ed3ab7d256" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "async def embed_async():\n", + " async_service = OrchestrationService()\n", + "\n", + " response = await async_service.aembed(\n", + " config=embeddings_config,\n", + " input=EmbeddingsInput(text=\"Hello async world!\")\n", + " )\n", + "\n", + " print(f\"Async embedding dimensions: {len(response.final_result.data[0].embedding)}\")\n", + " await async_service.aclose_http_connection()\n", + "\n", + "await embed_async()" + ], + "id": "caa35ae2d459f1c" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "85e7b848963d74b2" + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/prompt-registry.ipynb b/packages/gen/docs/gen_ai_hub/examples/prompt-registry.ipynb new file mode 100644 index 0000000..50ef859 --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/prompt-registry.ipynb @@ -0,0 +1,287 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(prompt_registry_notebook)=\n", + "# Prompt Registry\n", + "The Prompt Registry API allows you to create, manage, and retrieve prompt and orchestration config templates for use in SAP AICore when working with the models of the Generative AI Hub.\n", + "\n", + "In this notebook, we'll walk through the design-time process of creating prompt and config templates using the SDK. (This is also known as imperative Prompt Template creation.) Here, one can create and modify the Prompt Template and Orchestration Config Template. It will be versioned automatically and the versions can be retrieved by an id.\n", + "\n", + "See [SAP Help](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/prompt-registry?locale=en-US) for the difference between **imperative** and **declarative** prompt templates.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisite\n", + "### Provide the credentials to authenticate the client and establish a connection with the Prompt Registry API.\n", + "See options for providing credentials [here](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/_reference/README_sphynx.html#configuration-files)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prompt Template Management\n", + "### Step 0: Initialize Client\n", + "#### Initialize the client to interact with the Prompt Registry." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.prompt_registry import PromptTemplateClient\n", + "proxy_client = get_proxy_client(proxy_version=\"gen-ai-hub\")\n", + "prompt_registry_client = PromptTemplateClient(proxy_client=proxy_client)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Create Prompt Templates\n", + "#### Define the Prompt Template configuration and post to Prompt Registry." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from gen_ai_hub.prompt_registry import PromptTemplateSpec, PromptTemplate\n", + "\n", + "prompt_template_spec = PromptTemplateSpec(template=[PromptTemplate(role='system', content='You are a helpful assistant.')])\n", + "# PromptTemplateSpec also could include response_format, tools, defaults and additional_fields\n", + "\n", + "template_id = prompt_registry_client.create_prompt_template(\n", + " scenario='MyScenario', name='prompt_template_name', version='1.0.0', prompt_template_spec=prompt_template_spec).id\n", + "\n", + "print(f\"Created Prompt Template with ID: {template_id}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 2: Retrieve Prompt Templates\n", + "#### Retrieve the Prompt Template by ID." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = prompt_registry_client.get_prompt_template_by_id(template_id)\n", + "print(response.spec.template)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Modify the Prompt Template\n", + "#### We will add an input variable to the existing Prompt Template." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "prompt_template_spec = PromptTemplateSpec(template=[PromptTemplate(role='system',\n", + " content='You are a helpful assistant for {{ ?topic }}.'\n", + " )\n", + " ]\n", + " )\n", + "response = prompt_registry_client.create_prompt_template(scenario='MyScenario', name='prompt_template_name', version='1.0.0',\n", + " prompt_template_spec=prompt_template_spec)\n", + "\n", + "input_template_id = response.id\n", + "print(response.message)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Prompt Template History\n", + "#### Retrieve the history of Prompt Templates by scenario, name and version." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = prompt_registry_client.get_prompt_template_history(scenario='MyScenario', name='prompt_template_name', version='1.0.0')\n", + "print(response.json())" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5: Fill Prompt Template\n", + "#### Fill the variables in the Prompt Template." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "response = prompt_registry_client.fill_prompt_template_by_id(template_id=input_template_id, input_params={\"topic\": \"chemistry\"})\n", + "print(response.parsed_prompt)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "(orchestration_config_api)=\n", + "## Orchestration Config Management\n", + "### Step 0: Initialize Client\n", + "#### Initialize the client to interact with the Prompt Registry." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.prompt_registry import OrchestrationConfigClient\n", + "proxy_client = get_proxy_client(proxy_version=\"gen-ai-hub\")\n", + "prompt_registry_client = OrchestrationConfigClient(proxy_client=proxy_client)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 1: Create Orchestration Config\n", + "#### Define the Orchestration Config configuration and post to Prompt Registry." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from gen_ai_hub.orchestration_v2 import (OrchestrationConfig, ModuleConfig, LLMModelDetails, UserMessage, Template,\n", + " PromptTemplatingModuleConfig)\n", + "\n", + "config_spec = OrchestrationConfig(\n", + " modules=ModuleConfig(\n", + " prompt_templating=PromptTemplatingModuleConfig(\n", + " prompt=Template(template=[UserMessage(content=\"Hello, World!\")]),\n", + " model=LLMModelDetails(name=\"gpt-4o-mini\")\n", + " )\n", + " )\n", + " )\n", + "\n", + "template_id = prompt_registry_client.create_orchestration_config(\n", + " scenario='MyScenario', name='prompt_template_name', version='1.0.0', spec=config_spec).id\n", + "\n", + "print(f\"Created Orchestration Config Template with ID: {template_id}\")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 2: Retrieve Orchestration Config\n", + "#### Retrieve the Orchestration Config by ID." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "response = prompt_registry_client.get_orchestration_config_by_id(template_id)\n", + "print(response.spec)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "#### Retrieve the Orchestration Configs by scenario, name and version.." + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "response = prompt_registry_client.get_orchestration_configs(scenario='MyScenario', name='prompt_template_name', version='1.0.0')\n", + "print(response.resources)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Step 3: Orchestration Config History\n", + "#### Retrieve the history of Orchestration Config by scenario, name and version." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "response = prompt_registry_client.get_orchestration_configs(scenario='MyScenario', name='prompt_template_name', version='1.0.0')\n", + "print(response.resources)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Export Orchestration Config\n", + "#### Export a design orchestration config in a declarative compatible yaml file." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "response = prompt_registry_client.export_orchestration_config(config_id=template_id)\n", + "print(response)" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "language_info": { + "name": "python" + }, + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/gen/docs/gen_ai_hub/examples/streaming.ipynb b/packages/gen/docs/gen_ai_hub/examples/streaming.ipynb new file mode 100644 index 0000000..bd6894d --- /dev/null +++ b/packages/gen/docs/gen_ai_hub/examples/streaming.ipynb @@ -0,0 +1,429 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "47ce134df065fbc5", + "metadata": {}, + "source": [ + "# Streaming\n", + "\n", + "Streaming in AI models enables real-time data generation. With native SDKs, invocation and response formats vary by provider and model. Langchain simplifies this by offering a unified stream method." + ] + }, + { + "cell_type": "markdown", + "id": "b72c49bbc15de92c", + "metadata": {}, + "source": [ + "## Native SDKs" + ] + }, + { + "cell_type": "markdown", + "id": "3f45fe2bccb8331d", + "metadata": {}, + "source": [ + "### OpenAI - ChatGPT\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd93ddfba6ad306f", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:30.094794Z", + "start_time": "2024-08-15T06:57:30.087113Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.native.openai import chat\n", + "\n", + "def stream_openai(prompt, model_name='gpt-4o-mini'):\n", + " messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": prompt}\n", + " ]\n", + " \n", + " kwargs = dict(model_name=model_name, messages=messages, max_tokens=500, stream=True)\n", + " stream = chat.completions.create(**kwargs)\n", + " \n", + " for chunk in stream:\n", + " if chunk.choices:\n", + " content = chunk.choices[0].delta.content\n", + " if content:\n", + " print(content, end='')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af00d8971b879694", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:31.187724Z", + "start_time": "2024-08-15T06:57:30.384015Z" + } + }, + "outputs": [], + "source": [ + "stream_openai(\"Why is the sky blue?\")" + ] + }, + { + "cell_type": "markdown", + "id": "8912a320edfae6c0", + "metadata": {}, + "source": [ + "#### Structured model outputs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96eba48d11ec59b3", + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-19T12:07:25.266695Z", + "start_time": "2025-08-19T12:07:24.275850Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.proxy.native.openai import chat, OpenAI\n", + "from pydantic import BaseModel\n", + "\n", + "class Person(BaseModel):\n", + " name: str\n", + " age: int\n", + "\n", + "messages = [{\"role\": \"user\", \"content\": \"Tell me about John Doe, aged 30.\"}]\n", + "\n", + "def stream_openai_structured_outputs(messages, response_object, model_name):\n", + " # For more information, see:\n", + " # https://www.github.com/openai/openai-python#with_streaming_response and\n", + " # https://platform.openai.com/docs/guides/structured-outputs#streaming\n", + " with chat.completions.with_streaming_response.parse(\n", + " model=model_name,\n", + " messages=messages,\n", + " response_format=Person\n", + " ) as stream:\n", + " response = stream.parse() # takes care of the stream chunks and returns the final response\n", + " return response.choices[0].message.parsed\n", + "\n", + "print(stream_openai_structured_outputs(messages, Person, \"gpt-4o-mini\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "555490adf6b74f48", + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-19T12:13:29.130914Z", + "start_time": "2025-08-19T12:13:28.161449Z" + } + }, + "outputs": [], + "source": [ + "\n", + "def stream_beta_openai_structured_outputs(messages, response_object, model_name):\n", + " chat = OpenAI(proxy_client=get_proxy_client())\n", + " with chat.beta.chat.completions.stream(\n", + " model=model_name,\n", + " messages=messages,\n", + " response_format=Person\n", + " ) as stream:\n", + " response = stream.get_final_completion() # This will wait for the full response to be received\n", + " return response.choices[0].message.parsed\n", + "\n", + "print(stream_beta_openai_structured_outputs(messages, Person, \"gpt-4o-mini\"))" + ] + }, + { + "cell_type": "markdown", + "id": "6f3090d10d15ee5a", + "metadata": {}, + "source": [ + "### Google - GenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f66654acdebdfbc3", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:41.092063Z", + "start_time": "2024-08-15T06:57:34.855938Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy import get_proxy_client\n", + "from gen_ai_hub.proxy.native.google_genai import Client\n", + "from google.genai.types import GenerateContentConfig\n", + "\n", + "def stream_genai(prompt, model_name='gemini-2.0-flash'):\n", + " proxy_client = get_proxy_client('gen-ai-hub')\n", + " client = Client(proxy_client=proxy_client)\n", + " response = client.models.generate_content_stream(\n", + " model=model_name,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(max_output_tokens=500),\n", + " )\n", + " for chunk in response:\n", + " print(chunk.text, end='')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5deee97226b37ad2", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:43.282182Z", + "start_time": "2024-08-15T06:57:41.093075Z" + } + }, + "outputs": [], + "source": [ + "stream_genai(\"Why is the sky blue?\")" + ] + }, + { + "cell_type": "markdown", + "id": "a584a1954daff27a", + "metadata": {}, + "source": [ + "### Anthropic - Claude" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3becb5bcc8179642", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:46.033938Z", + "start_time": "2024-08-15T06:57:45.759219Z" + } + }, + "outputs": [], + "source": [ + "import json\n", + "from gen_ai_hub.proxy.native.amazon import Session\n", + "\n", + "def stream_claude(prompt, model_name='anthropic--claude-3-haiku'):\n", + " bedrock = Session().client(model_name=model_name)\n", + " body = json.dumps({\n", + " \"max_tokens\": 500,\n", + " \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n", + " \"anthropic_version\": \"bedrock-2023-05-31\"\n", + " })\n", + " \n", + " response = bedrock.invoke_model_with_response_stream(body=body)\n", + " stream = response.get(\"body\")\n", + " \n", + " for event in stream:\n", + " chunk = json.loads(event[\"chunk\"][\"bytes\"])\n", + " if chunk[\"type\"] == \"content_block_delta\":\n", + " print(chunk[\"delta\"].get(\"text\", \"\"), end=\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b39e55cc3ecd3eb3", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:50.526221Z", + "start_time": "2024-08-15T06:57:46.639287Z" + } + }, + "outputs": [], + "source": [ + "stream_claude(\"Why is the sky blue?\")" + ] + }, + { + "cell_type": "markdown", + "id": "f62c196b89fe0a2", + "metadata": {}, + "source": [ + "### Amazon - Bedrock" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccaf0badc509a272", + "metadata": { + "ExecuteTime": { + "end_time": "2025-11-17T18:24:10.254876Z", + "start_time": "2025-11-17T18:24:10.251421Z" + } + }, + "outputs": [], + "source": [ + "import json\n", + "from gen_ai_hub.proxy.native.amazon import Session\n", + "\n", + "def stream_bedrock(prompt, model_name='amazon--nova-pro'):\n", + " bedrock = Session().client(model_name=model_name)\n", + " body = json.dumps({\n", + " \"schemaVersion\": \"messages-v1\",\n", + " \"messages\": [{\"role\": \"user\", \"content\": [{\"text\": prompt}]}],\n", + " \"system\": [{\"text\": \"Act as a creative writing assistant.\"}],\n", + " \"inferenceConfig\": {\"maxTokens\": 500, \"topP\": 0.9, \"topK\": 20, \"temperature\": 0.7},\n", + " })\n", + "\n", + " response = bedrock.invoke_model_with_response_stream(body=body)\n", + " stream = response.get(\"body\")\n", + " chunk_count = 0\n", + " answer = \"\"\n", + " if stream:\n", + " for event in stream:\n", + " chunk = event.get(\"chunk\")\n", + " if chunk:\n", + " chunk_json = json.loads(chunk.get(\"bytes\").decode())\n", + " content_block_delta = chunk_json.get(\"contentBlockDelta\")\n", + " if content_block_delta:\n", + " chunk_count += 1\n", + " answer += content_block_delta.get(\"delta\").get(\"text\")\n", + " print(f\"Total chunks: {chunk_count}\")\n", + " print(\"Final answer:\", answer)\n", + " else:\n", + " print(\"No response stream received.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ce343f9a7f15b81", + "metadata": {}, + "outputs": [], + "source": [ + "stream_bedrock(\"Why is the sky blue?\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5f696a1f2959506", + "metadata": {}, + "source": [ + "## Langchain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94e2198c6a2c5b39", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:57.259042Z", + "start_time": "2024-08-15T06:57:56.012904Z" + } + }, + "outputs": [], + "source": [ + "from gen_ai_hub.proxy.langchain import init_llm\n", + "\n", + "def stream_langchain(prompt, model_name):\n", + " llm = init_llm(model_name=model_name, max_tokens=500)\n", + " \n", + " for chunk in llm.stream(prompt):\n", + " print(chunk.content, end='')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "112f9befe343c559", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:57:59.402267Z", + "start_time": "2024-08-15T06:57:57.493817Z" + } + }, + "outputs": [], + "source": [ + "stream_langchain(\"How do airplanes stay in the air?\", model_name='gpt-4o-mini')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db930c7fbd145b3e", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:58:02.318174Z", + "start_time": "2024-08-15T06:57:59.403573Z" + } + }, + "outputs": [], + "source": [ + "stream_langchain(\"How do airplanes stay in the air?\", model_name='gemini-2.0-flash')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c74c6bcb6e5031f7", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:58:06.688739Z", + "start_time": "2024-08-15T06:58:02.319370Z" + } + }, + "outputs": [], + "source": [ + "stream_langchain(\"How do airplanes stay in the air?\", model_name='anthropic--claude-3-haiku')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "602727294c914391", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-15T06:58:08.682152Z", + "start_time": "2024-08-15T06:58:06.689631Z" + } + }, + "outputs": [], + "source": [ + "stream_langchain(\"How do airplanes stay in the air?\", model_name='amazon--nova-premier')" + ] + }, + { + "cell_type": "markdown", + "id": "77e5d9ce123c8e9b", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/gen/docs/source/_static/custom.css b/packages/gen/docs/source/_static/custom.css new file mode 100644 index 0000000..b83ef55 --- /dev/null +++ b/packages/gen/docs/source/_static/custom.css @@ -0,0 +1,115 @@ +/* Light theme */ +:root { + --mystnb-source-bg-color: #f8fafc; + --mystnb-source-color: #334155; + --mystnb-stdout-bg-color: #f0f7ff; + --mystnb-output-border-color: #bfdbfe; + --mystnb-border-color: #e2e8f0; +} + +/* Dark theme */ +.dark { + --mystnb-source-bg-color: #1e1e2e; + --mystnb-source-color: #cdd6f4; + --mystnb-stdout-bg-color: #171731; + --mystnb-output-border-color: #2e2e72; + --mystnb-border-color: #313244; +} + +.cell_input { + border: 1px solid var(--mystnb-border-color) !important; + border-radius: 8px; + margin: 1.5rem 0; + padding: 1px; +} + +.cell_output { + border: 1px solid var(--mystnb-output-border-color); + border-radius: 8px; + margin: 1.5rem 0; + padding: 1px; + overflow: hidden; + background: var(--mystnb-stdout-bg-color); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.cell_output .output.stream { + border: none; + margin: 0; + border-radius: 8px; +} + +.cell_input:focus-within { + outline: 2px solid var(--mystnb-source-color); + outline-offset: 2px; +} + +.highlight { + background-color: var(--mystnb-source-bg-color); + color: var(--mystnb-source-color); + scrollbar-width: thin; + scrollbar-color: var(--mystnb-source-color) transparent; + border-radius: 6px; + margin: 0; +} + +.cell_output .highlight { + background-color: var(--mystnb-stdout-bg-color); + border: none; + border-radius: 8px; + margin: -1px; +} + +.highlight::-webkit-scrollbar { + height: 6px; +} + +.highlight::-webkit-scrollbar-thumb { + background: var(--mystnb-source-color); + border-radius: 3px; +} + +/* Dark theme syntax */ +.dark .highlight .c1 { color: #7f849c; } +.dark .highlight .s1, .dark .highlight .s2 { color: #89dceb; } +.dark .highlight .k, .dark .highlight .kn { color: #f5c2e7; } +.dark .highlight .n { color: #cdd6f4; } +.dark .highlight .o { color: #89b4fa; } +.dark .highlight .p { color: #9399b2; } + +/* Light theme syntax */ +.highlight .c1 { color: #64748b; } +.highlight .s1, .highlight .s2 { color: #0369a1; } +.highlight .k, .highlight .kn { color: #be185d; } +.highlight .n { color: #334155; } +.highlight .o { color: #0284c7; } +.highlight .p { color: #475569; } + +button.copy { + background: transparent; + border: none; + opacity: 0.25; + transition: opacity 0.2s ease; +} + +button.copy:hover { + opacity: 1; +} + +/* autodoc related adjustments*/ + +/* autodoc generation for docstring causes no wordwrap */ +.sig-name, .viewcode-link, .py-attribute, .py-class, .py-function, .descclassname { + word-break: break-all; + white-space: normal !important; +} + +/* break word and avoid horizontal scrollbar */ +#left-sidebar { + word-break: break-word; +} + +main{ + word-wrap: break-word; +} + diff --git a/packages/gen/docs/source/conf.py b/packages/gen/docs/source/conf.py new file mode 100644 index 0000000..a9319dd --- /dev/null +++ b/packages/gen/docs/source/conf.py @@ -0,0 +1,99 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +import pathlib +import sys +sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) + +# -- Project information ----------------------------------------------------- + +project = 'SAP Cloud SDK for AI (Python) - generative' +copyright = '2026, SAP SE' +author = 'SAP SE' + +# The full version, including alpha/beta/rc tags +VERSION_FILE = '../../version.txt' + +def get_version(version_file=VERSION_FILE): + with open(version_file, encoding='utf-8-sig', mode='r') as ver_file: + version_str = ver_file.readline().rstrip() + return version_str + +release = get_version() + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.duration', + 'sphinx.ext.autodoc', + 'myst_nb', +] + +nb_execution_mode = "off" +nb_execution_allow_errors=True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinxawesome_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] +html_css_files = ['custom.css'] + +# Disable (`--`) are being converted into em dashes (`—`). +smartquotes = False + +html_title = "SAP Cloud SDK for AI (Python) - generative v" + release + +html_permalinks = False + + +# -- Options for autodoc ---------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration + +# Automatically extract typehints when specified and place them in +# descriptions of the relevant function/method. +autodoc_typehints = "description" + +# Don't show class signature with the class' name. +autodoc_class_signature = "separated" + +# Shorten the names of documented modules by removing the given prefix. +add_module_names = False + +# Format typehints using 'short' notation (e.g., 'list' instead of 'typing.List') +autodoc_typehints_format = 'short' + +# group members by type (e.g. all methods together) +autodoc_member_order = 'groupwise' + +# Don't show the module name before each documented member. +add_module_names = False + +# Remove the given prefix from module names in the documentation. +modindex_common_prefix = ['gen_ai_hub'] diff --git a/packages/gen/docs/source/examples.rst b/packages/gen/docs/source/examples.rst new file mode 100644 index 0000000..66bfb51 --- /dev/null +++ b/packages/gen/docs/source/examples.rst @@ -0,0 +1,13 @@ +Examples +======== + +.. toctree:: + ./_reference/gen_ai_hub + ./_reference/streaming + ./_reference/prompt-registry + ./_reference/orchestration-service + ./_reference/orchestration-service2 + ./_reference/document-grounding + ./_reference/async-examples + ./_reference/evaluations + diff --git a/packages/gen/docs/source/index.rst b/packages/gen/docs/source/index.rst new file mode 100644 index 0000000..e00685c --- /dev/null +++ b/packages/gen/docs/source/index.rst @@ -0,0 +1,13 @@ +.. Generative AI Hub SDK documentation master file, created by + sphinx-quickstart on Thu Oct 10 10:21:45 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +SAP Cloud SDK for AI (Python) - generative +===================================================== + +.. toctree:: + Introduction <./_reference/README_sphynx.md> + examples + Release Notes <./_reference/RELEASE_NOTES.md> + API Reference <./_api_doc/gen_ai_hub.rst> \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/__init__.py b/packages/gen/gen_ai_hub/__init__.py new file mode 100644 index 0000000..21b0cc9 --- /dev/null +++ b/packages/gen/gen_ai_hub/__init__.py @@ -0,0 +1,3 @@ +from .proxy.gen_ai_hub_proxy import GenAIHubProxyClient + +__all__ = ['GenAIHubProxyClient'] diff --git a/packages/gen/gen_ai_hub/batch_service/__init__.py b/packages/gen/gen_ai_hub/batch_service/__init__.py new file mode 100644 index 0000000..48bd8ce --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/__init__.py @@ -0,0 +1,31 @@ +from .models import * +from .service import BatchService +from .exceptions import BatchServiceError + +__all__ = [ + # request + "BatchCreateRequest", + "BatchInput", + "BatchOutput", + "BatchSpec", + + # response + "BatchStatus", + "BatchCreateResponse", + "BatchSummary", + "BatchListResponse", + "BatchStatusDetail", + "BatchInputDetail", + "BatchOutputDetail", + "BatchDetailResponse", + "BatchStatusResponse", + "BatchCancelResponse", + "BatchDeleteResponse", + "ErrorResponse", + + # service + "BatchService", + + # exceptions + "BatchServiceError", +] diff --git a/packages/gen/gen_ai_hub/batch_service/exceptions.py b/packages/gen/gen_ai_hub/batch_service/exceptions.py new file mode 100644 index 0000000..9179723 --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/exceptions.py @@ -0,0 +1,29 @@ +""" +Exceptions for the batch service module. +""" + +import httpx + + +class BatchServiceError(Exception): + """ + Raised when the batch service returns an error response. + + Captures the request_id from the error payload for tracing. + """ + + def __init__( + self, + request_id: str, + message: str, + status_code: int, + headers: httpx.Headers, + ): + self.request_id = request_id + self.message = message + self.status_code = status_code + self.headers = headers + super().__init__(message) + + +__all__ = ["BatchServiceError"] diff --git a/packages/gen/gen_ai_hub/batch_service/models/__init__.py b/packages/gen/gen_ai_hub/batch_service/models/__init__.py new file mode 100644 index 0000000..7f2f837 --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/models/__init__.py @@ -0,0 +1,40 @@ +from .base import ABCBaseModel, ResponseBaseModel +from .request import BatchCreateRequest, BatchInput, BatchOutput, BatchSpec +from .response import ( + BatchStatus, + BatchCreateResponse, + BatchSummary, + BatchListResponse, + BatchStatusDetail, + BatchInputDetail, + BatchOutputDetail, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, + BatchDeleteResponse, + ErrorResponse, +) + +__all__ = [ + # base + "ABCBaseModel", + "ResponseBaseModel", + # request + "BatchCreateRequest", + "BatchInput", + "BatchOutput", + "BatchSpec", + # response + "BatchStatus", + "BatchCreateResponse", + "BatchSummary", + "BatchListResponse", + "BatchStatusDetail", + "BatchInputDetail", + "BatchOutputDetail", + "BatchDetailResponse", + "BatchStatusResponse", + "BatchCancelResponse", + "BatchDeleteResponse", + "ErrorResponse", +] diff --git a/packages/gen/gen_ai_hub/batch_service/models/base.py b/packages/gen/gen_ai_hub/batch_service/models/base.py new file mode 100644 index 0000000..658026d --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/models/base.py @@ -0,0 +1,31 @@ +from abc import ABC + +from pydantic import BaseModel, ConfigDict + + +class ABCBaseModel(BaseModel, ABC): + """ + Abstract base model for batch service request models. + + - `extra="forbid"` rejects unexpected fields. + - `by_alias=True` / `exclude_none=True` ensure clean API payloads. + """ + + model_config = ConfigDict( + extra="forbid", + frozen=False, + ) + + def model_dump(self, **kwargs): + kwargs.setdefault("by_alias", True) + kwargs.setdefault("exclude_none", True) + return super().model_dump(**kwargs) + + +class ResponseBaseModel(BaseModel): + """Base model for API response models — allows extra fields for forward compatibility.""" + + model_config = ConfigDict( + extra="allow", + frozen=False, + ) diff --git a/packages/gen/gen_ai_hub/batch_service/models/request.py b/packages/gen/gen_ai_hub/batch_service/models/request.py new file mode 100644 index 0000000..5b88d7b --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/models/request.py @@ -0,0 +1,75 @@ +""" +Request models for the LLM Batch Service API. +""" + +from typing import Literal + +from pydantic import Field + +from gen_ai_hub.batch_service.models.base import ABCBaseModel + + +class BatchInput(ABCBaseModel): + """Input configuration for a batch job. + + Points to the ``.jsonl`` file in an object store that contains the + individual LLM requests to be processed. + + :param uri: Fully qualified object-store URI of the input file. + Must point to a ``.jsonl`` file (e.g. ``ai://my-store/input/requests.jsonl``). + :type uri: str + """ + + uri: str = Field(..., description="Input file URI (must be a .jsonl file)") + + +class BatchOutput(ABCBaseModel): + """Output configuration for a batch job. + + Points to the directory in an object store where results will be written + once the job completes. + + :param uri: Fully qualified object-store URI of the output directory + (e.g. ``ai://my-store/output/``). + :type uri: str + """ + + uri: str = Field(..., description="Output directory URI") + + +class BatchSpec(ABCBaseModel): + """Specification of the LLM to use for a batch job. + + :param provider: LLM provider name as registered in SAP AI Core + (e.g. ``"azure-openai"``). + :type provider: str + :param model: Model name to use for inference + (e.g. ``"gpt-4.1-mini"``). + :type model: str + """ + + provider: str = Field(..., description="LLM provider name") + model: str = Field(..., description="Model name") + + +class BatchCreateRequest(ABCBaseModel): + """Request body sent to ``POST /llm-batch-service/v1/batches``. + + Describes a new batch processing job: where to read input from, where to + write output, and which model to use. + + :param type: Batch processing type. Currently only ``"llm-native"`` is + supported. + :type type: Literal["llm-native"] + :param input: Input file configuration. + :type input: :class:`BatchInput` + :param output: Output directory configuration. + :type output: :class:`BatchOutput` + :param spec: LLM provider and model specification. + :type spec: :class:`BatchSpec` + """ + + type: Literal["llm-native"] = Field(..., description="Type of batch processing") + input: BatchInput = Field(..., description="Input file configuration") + output: BatchOutput = Field(..., description="Output directory configuration") + spec: BatchSpec = Field(..., description="Batch job specification") diff --git a/packages/gen/gen_ai_hub/batch_service/models/response.py b/packages/gen/gen_ai_hub/batch_service/models/response.py new file mode 100644 index 0000000..88c6ba3 --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/models/response.py @@ -0,0 +1,232 @@ +""" +Response models for the LLM Batch Service API. +""" + +from enum import Enum +from typing import Optional + +from pydantic import Field + +from gen_ai_hub.batch_service.models.base import ResponseBaseModel + + +class BatchStatus(str, Enum): + """Enumeration of possible lifecycle states for a batch job. + + :cvar PENDING: Job has been accepted and is waiting to be scheduled. + :cvar RUNNING: Job is actively being processed. + :cvar COMPLETED: Job finished successfully. + :cvar FAILED: Job terminated with an error. + :cvar CANCELLED: Job was cancelled by the user. + :cvar CANCELLING: Cancellation has been requested and is in progress. + """ + + PENDING = "PENDING" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + CANCELLED = "CANCELLED" + CANCELLING = "CANCELLING" + + +class BatchCreateResponse(ResponseBaseModel): + """Response returned by ``POST /llm-batch-service/v1/batches``. + + Confirms that the batch job has been accepted and provides the assigned + identifier and initial status. + + :param id: Unique identifier (UUID) of the created batch job. + :type id: str + :param created_at: ISO 8601 timestamp of when the job was created. + :type created_at: str, optional + :param status: Initial status of the job, typically ``"PENDING"``. + :type status: str, optional + :param message: Human-readable confirmation message from the service. + :type message: str, optional + """ + + id: str = Field(..., description="Unique identifier of the batch job") + created_at: Optional[str] = Field(None, description="ISO 8601 creation timestamp") + status: Optional[str] = Field(None, description="Initial job status") + message: Optional[str] = Field(None, description="Human-readable status message") + + +class BatchSummary(ResponseBaseModel): + """Summary entry for a single batch job as returned in a list response. + + :param id: Unique identifier (UUID) of the batch job. + :type id: str + :param type: Batch processing type (e.g. ``"llm-native"``). + :type type: str, optional + :param provider: LLM provider name (e.g. ``"azure-openai"``). + :type provider: str, optional + :param created_at: ISO 8601 timestamp of when the job was created. + :type created_at: str, optional + :param status: Current status of the job. + :type status: str, optional + """ + + id: str = Field(..., description="Unique identifier of the batch job") + type: Optional[str] = Field(None, description="Batch processing type") + provider: Optional[str] = Field(None, description="LLM provider name") + created_at: Optional[str] = Field(None, description="ISO 8601 creation timestamp") + status: Optional[str] = Field(None, description="Current job status") + + +class BatchListResponse(ResponseBaseModel): + """Response returned by ``GET /llm-batch-service/v1/batches``. + + Contains a count and a list of batch job summaries for the current + resource group. + + :param count: Total number of batch jobs. + :type count: int, optional + :param resources: List of batch job summaries. + :type resources: list[:class:`BatchSummary`], optional + """ + + count: Optional[int] = Field(None, description="Total number of batch jobs") + resources: Optional[list[BatchSummary]] = Field(None, description="List of batch job summaries") + + +class BatchStatusDetail(ResponseBaseModel): + """Status block embedded inside :class:`BatchDetailResponse`. + + :param current_status: The job's current lifecycle status. + :type current_status: str, optional + :param target_status: The terminal status the job is expected to reach. + :type target_status: str, optional + :param updated_at: ISO 8601 timestamp of the last status change. + :type updated_at: str, optional + :param message: Optional human-readable description of the current status. + :type message: str, optional + """ + + current_status: Optional[str] = Field(None, description="Current job status") + target_status: Optional[str] = Field(None, description="Target terminal status") + updated_at: Optional[str] = Field(None, description="ISO 8601 timestamp of last status update") + message: Optional[str] = Field(None, description="Optional human-readable status message") + + +class BatchInputDetail(ResponseBaseModel): + """Input configuration as returned in a batch detail response. + + :param uri: Object-store URI of the input ``.jsonl`` file. + :type uri: str, optional + """ + + uri: Optional[str] = Field(None, description="Input file URI") + + +class BatchOutputDetail(ResponseBaseModel): + """Output configuration as returned in a batch detail response. + + :param uri: Object-store URI of the output directory. + :type uri: str, optional + """ + + uri: Optional[str] = Field(None, description="Output directory URI") + + +class BatchDetailResponse(ResponseBaseModel): + """Response returned by ``GET /llm-batch-service/v1/batches/{batch_id}``. + + Provides the full configuration and current status of a specific batch job. + + :param id: Unique identifier (UUID) of the batch job. + :type id: str, optional + :param type: Batch processing type (e.g. ``"llm-native"``). + :type type: str, optional + :param provider: LLM provider name (e.g. ``"azure-openai"``). + :type provider: str, optional + :param created_at: ISO 8601 timestamp of when the job was created. + :type created_at: str, optional + :param input: Input file configuration. + :type input: :class:`BatchInputDetail`, optional + :param output: Output directory configuration. + :type output: :class:`BatchOutputDetail`, optional + :param spec: Raw job specification dict as stored by the service. + :type spec: dict, optional + :param status: Current status details. + :type status: :class:`BatchStatusDetail`, optional + """ + + id: Optional[str] = Field(None, description="Unique identifier of the batch job") + type: Optional[str] = Field(None, description="Batch processing type") + provider: Optional[str] = Field(None, description="LLM provider name") + created_at: Optional[str] = Field(None, description="ISO 8601 creation timestamp") + input: Optional[BatchInputDetail] = Field(None, description="Input configuration") + output: Optional[BatchOutputDetail] = Field(None, description="Output configuration") + spec: Optional[dict] = Field(None, description="Batch job specification") + status: Optional[BatchStatusDetail] = Field(None, description="Current status details") + + +class BatchStatusResponse(ResponseBaseModel): + """Response returned by ``GET /llm-batch-service/v1/batches/{batch_id}/status``. + + :param current_status: The job's current lifecycle status. + :type current_status: str, optional + :param target_status: The terminal status the job is expected to reach. + :type target_status: str, optional + :param updated_at: ISO 8601 timestamp of the last status change. + :type updated_at: str, optional + :param message: Optional human-readable description of the current status. + :type message: str, optional + """ + + current_status: Optional[str] = Field(None, description="Current job status") + target_status: Optional[str] = Field(None, description="Target terminal status") + updated_at: Optional[str] = Field(None, description="ISO 8601 timestamp of last status update") + message: Optional[str] = Field(None, description="Optional human-readable status message") + + +class BatchCancelResponse(ResponseBaseModel): + """Response returned by ``PATCH /llm-batch-service/v1/batches/{batch_id}/cancel``. + + Confirms that the cancellation request has been accepted. The job will + transition to ``CANCELLING`` and eventually ``CANCELLED``. + + :param id: Unique identifier (UUID) of the batch job. + :type id: str, optional + :param created_at: ISO 8601 timestamp of when the job was originally created. + :type created_at: str, optional + :param message: Human-readable confirmation that cancellation was scheduled. + :type message: str, optional + """ + + id: Optional[str] = Field(None, description="Unique identifier of the batch job") + created_at: Optional[str] = Field(None, description="ISO 8601 creation timestamp") + message: Optional[str] = Field(None, description="Human-readable confirmation message") + + +class BatchDeleteResponse(ResponseBaseModel): + """Response returned by ``DELETE /llm-batch-service/v1/batches/{batch_id}``. + + Confirms that the batch job record has been deleted. Only jobs in a + terminal state (``COMPLETED``, ``FAILED``, or ``CANCELLED``) can be deleted. + + :param id: Unique identifier (UUID) of the deleted batch job. + :type id: str, optional + :param created_at: ISO 8601 timestamp of when the job was originally created. + :type created_at: str, optional + :param message: Human-readable confirmation of the deletion. + :type message: str, optional + """ + + id: Optional[str] = Field(None, description="Unique identifier of the batch job") + created_at: Optional[str] = Field(None, description="ISO 8601 creation timestamp") + message: Optional[str] = Field(None, description="Human-readable confirmation message") + + +class ErrorResponse(ResponseBaseModel): + """Error response body returned by the batch service on 4xx/5xx responses. + + :param request_id: Unique request identifier, useful for tracing the + error in service logs. + :type request_id: str + :param message: Human-readable description of the error. + :type message: str + """ + + request_id: str = Field(..., description="Unique request identifier for tracing") + message: str = Field(..., description="Human-readable error message") diff --git a/packages/gen/gen_ai_hub/batch_service/service.py b/packages/gen/gen_ai_hub/batch_service/service.py new file mode 100644 index 0000000..cf37820 --- /dev/null +++ b/packages/gen/gen_ai_hub/batch_service/service.py @@ -0,0 +1,429 @@ +""" +Client for the LLM Batch Service API. + +Provides synchronous and asynchronous methods to create, list, inspect, +cancel, and delete batch processing jobs via SAP AI Core. +""" + +from typing import Optional, Union + +import httpx + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.batch_service.exceptions import BatchServiceError +from gen_ai_hub.batch_service.models.request import BatchCreateRequest, BatchInput, BatchOutput, BatchSpec +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, + BatchDeleteResponse, +) + +_BASE_PATH = "/llm-batch-service/v1/batches" + + +def _handle_http_error(response: httpx.Response) -> None: + """Raises BatchServiceError from a non-2xx httpx response.""" + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + try: + payload = response.json() + request_id = payload.get('request_id', '') + error_message = payload.get('message', response.text) + except Exception as exc: + raise error from exc + raise BatchServiceError( + request_id=request_id, + message=error_message, + status_code=response.status_code, + headers=response.headers, + ) + + +class BatchService: + """ + Client for the LLM Batch Service API. + + Supports synchronous and asynchronous variants of all five operations: + create, list, get, cancel, and delete batch jobs. + + The ``AI-Resource-Group`` header is injected automatically from + ``proxy_client.request_header`` on every request. + + + :param api_url: Base URL of the SAP AI Core API + (e.g. ``https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com/v2``). + Defaults to the URL resolved from ``proxy_client``. + :type api_url: str, Optional + :param proxy_client: A ``GenAIHubProxyClient`` instance. Defaults to the + result of ``get_proxy_client(proxy_version="gen-ai-hub")``. + :type proxy_client: :class:`GenAIHubProxyClient` + :param resource_group: Value for the ``AI-Resource-Group`` header. Falls back + to the resource group on ``proxy_client`` when omitted. + :type resource_group: str, Optional + :param timeout: Default HTTP request timeout passed to httpx. + :type timeout: Union[int, float, httpx.Timeout], Optional + """ + + def __init__( + self, + api_url: Optional[str] = None, + proxy_client: Optional[GenAIHubProxyClient] = None, + resource_group: Optional[str] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ): + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + if api_url: + self.api_url = api_url.rstrip("/") + else: + base = self.proxy_client.ai_core_client.base_url.rstrip("/") + self.api_url = base + self.resource_group = resource_group + self.timeout = timeout + self.client = httpx.Client(timeout=self.timeout) + self.async_client = httpx.AsyncClient(timeout=self.timeout) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _headers(self) -> dict: + headers = dict(self.proxy_client.request_header) + if self.resource_group: + headers["AI-Resource-Group"] = self.resource_group + return headers + + def _determine_timeout( + self, timeout: Union[int, float, httpx.Timeout, None] + ) -> Union[int, float, httpx.Timeout]: + if timeout is not None: + return timeout + if self.timeout is not None: + return self.timeout + return httpx.USE_CLIENT_DEFAULT + + def _batches_url(self, *segments: str) -> str: + parts = [self.api_url + _BASE_PATH] + list(segments) + return "/".join(p.strip("/") for p in parts if p) + + # ------------------------------------------------------------------ + # Sync API + # ------------------------------------------------------------------ + + def create( + self, + *, + type: str, + input_uri: str, + output_uri: str, + provider: str, + model: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchCreateResponse: + """Create a new batch processing job. + + :param type: Batch processing type (only ``"llm-native"`` is supported). + :type type: str + :param input_uri: URI of the input ``.jsonl`` file in the object store. + :type input_uri: str + :param output_uri: URI of the output directory in the object store. + :type output_uri: str + :param provider: LLM provider name (e.g. ``"azure-openai"``). + :type provider: str + :param model: Model name (e.g. ``"gpt-4.1-mini"``). + :type model: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchCreateResponse` with the job ID and initial status. + """ + body = BatchCreateRequest( + type=type, + input=BatchInput(uri=input_uri), + output=BatchOutput(uri=output_uri), + spec=BatchSpec(provider=provider, model=model), + ) + response = self.client.post( + self._batches_url(), + headers=self._headers(), + json=body.model_dump(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchCreateResponse(**response.json()) + + def list( + self, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchListResponse: + """List all batch jobs for the current resource group. + + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchListResponse` containing the batch summaries. + """ + response = self.client.get( + self._batches_url(), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchListResponse(**response.json()) + + def get( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchDetailResponse: + """Retrieve details of a specific batch job. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchDetailResponse` with full job details. + """ + response = self.client.get( + self._batches_url(batch_id), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchDetailResponse(**response.json()) + + def get_status( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchStatusResponse: + """Retrieve the current status of a batch job. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchStatusResponse` with current and target status. + """ + response = self.client.get( + self._batches_url(batch_id, "status"), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchStatusResponse(**response.json()) + + def cancel( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchCancelResponse: + """Schedule a batch job for cancellation. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchCancelResponse` confirming the cancellation request. + """ + response = self.client.patch( + self._batches_url(batch_id, "cancel"), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchCancelResponse(**response.json()) + + def delete( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchDeleteResponse: + """Delete a batch job (only allowed for terminal states: COMPLETED, FAILED, CANCELLED). + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchDeleteResponse` confirming the deletion. + """ + response = self.client.delete( + self._batches_url(batch_id), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchDeleteResponse(**response.json()) + + # ------------------------------------------------------------------ + # Async API + # ------------------------------------------------------------------ + + async def acreate( + self, + *, + type: str = "llm-native", + input_uri: str, + output_uri: str, + provider: str, + model: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchCreateResponse: + """Async variant of :meth:`create`. + + :param type: Batch processing type (only ``"llm-native"`` is supported). + :type type: str + :param input_uri: URI of the input ``.jsonl`` file in the object store. + :type input_uri: str + :param output_uri: URI of the output directory in the object store. + :type output_uri: str + :param provider: LLM provider name (e.g. ``"azure-openai"``). + :type provider: str + :param model: Model name (e.g. ``"gpt-4.1-mini"``). + :type model: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchCreateResponse` with the job ID and initial status. + """ + body = BatchCreateRequest( + type=type, + input=BatchInput(uri=input_uri), + output=BatchOutput(uri=output_uri), + spec=BatchSpec(provider=provider, model=model), + ) + response = await self.async_client.post( + self._batches_url(), + headers=self._headers(), + json=body.model_dump(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchCreateResponse(**response.json()) + + async def alist( + self, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchListResponse: + """Async variant of :meth:`list`. + + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchListResponse` containing the batch summaries. + """ + response = await self.async_client.get( + self._batches_url(), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchListResponse(**response.json()) + + async def aget( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchDetailResponse: + """Async variant of :meth:`get`. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchDetailResponse` with full job details. + """ + response = await self.async_client.get( + self._batches_url(batch_id), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchDetailResponse(**response.json()) + + async def aget_status( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchStatusResponse: + """Async variant of :meth:`get_status`. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchStatusResponse` with current and target status. + """ + response = await self.async_client.get( + self._batches_url(batch_id, "status"), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchStatusResponse(**response.json()) + + async def acancel( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchCancelResponse: + """Async variant of :meth:`cancel`. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchCancelResponse` confirming the cancellation request. + """ + response = await self.async_client.patch( + self._batches_url(batch_id, "cancel"), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchCancelResponse(**response.json()) + + async def adelete( + self, + batch_id: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> BatchDeleteResponse: + """Async variant of :meth:`delete`. + + :param batch_id: UUID of the batch job. + :type batch_id: str + :param timeout: Per-request timeout override. + :type timeout: Union[int, float, httpx.Timeout], Optional + :returns: :class:`BatchDeleteResponse` confirming the deletion. + """ + response = await self.async_client.delete( + self._batches_url(batch_id), + headers=self._headers(), + timeout=self._determine_timeout(timeout), + ) + if not response.is_success: + _handle_http_error(response) + return BatchDeleteResponse(**response.json()) + + # ------------------------------------------------------------------ + # Resource management + # ------------------------------------------------------------------ + + def close_http_connection(self) -> None: + """Close the underlying synchronous httpx client.""" + self.client.close() + + async def aclose_http_connection(self) -> None: + """Close the underlying asynchronous httpx client.""" + await self.async_client.aclose() diff --git a/packages/gen/gen_ai_hub/document_grounding/__init__.py b/packages/gen/gen_ai_hub/document_grounding/__init__.py new file mode 100644 index 0000000..9a68286 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/__init__.py @@ -0,0 +1,98 @@ +"""Document Grounding package for SAP Generative AI Hub. + +This package provides APIs for document grounding capabilities including: +- Pipeline management for document vectorization from various data sources +- Vector store operations for semantic search +- Retrieval operations for querying document repositories + +The package includes three main API clients: +- PipelineAPIClient: Manages document vectorization pipelines +- VectorAPIClient: Manages vector collections and semantic search +- RetrievalAPIClient: Performs retrieval operations across data repositories +""" +from .client import PipelineAPIClient, VectorAPIClient, RetrievalAPIClient +from .models import * + +__all__ = [ + # Pipeline models + "CreatePipelineRequest", + "MSSharePointPipelineCreateRequest", + "S3PipelineCreateRequest", + "SFTPPipelineCreateRequest", + "SearchPipelineRequest", + "DataRepositoryMetadataItem", + "CommonConfiguration", + "MetaData", + "MSSharePointConfiguration", + "SharePointConfig", + "SharePointSite", + "ManualPipelineTrigger", + "PipelineIdResponse", + "GetPipelineResponse", + "GetPipelinesResponse", + "GetPipelineStatusResponse", + "PipelineExecution", + "GetPipelineExecutionsResponse", + "Document", + "DocumentsStatusResponse", + "MSSharePointPipelineGetResponse", + "S3PipelineGetResponse", + "SFTPPipelineGetResponse", + "SearchPipelineData", + "SearchPipelinesResponse", + "PipelineExecutionStatus", + "DocumentStatus", + "BasePipelineResponse", + "MSSharePointConfigurationGetResponse", + # Retrieval models + "RetrievalKeyValueListPair", + "RetrievalDocumentKeyValueListPair", + "RetrievalSearchDocumentKeyValueListPair", + "RetrievalChunk", + "RetrievalDocument", + "DataRepositoryType", + "DataRepository", + "DataRepositoryWithDocuments", + "RetrievalSearchConfiguration", + "RetrievalSearchFilter", + "RetrievalSearchInput", + "RetrievalDataRepositorySearchResult", + "RetrievalPerFilterSearchResult", + "RetrievalPerFilterSearchResultError", + "RetrievalPerFilterSearchResultWithError", + "RetrievalSearchResults", + "DataRepositories", + # Vector models + "VectorKeyValueListPair", + "EmbeddingConfig", + "CollectionCreateRequest", + "Collection", + "CollectionsListResponse", + "TextOnlyBaseChunk", + "BaseDocument", + "DocumentWithoutChunks", + "VectorDocument", + "DocumentsCreateRequest", + "DocumentsUpdateRequest", + "DocumentsListResponse", + "DocumentsResponse", + "CollectionCreatedResponse", + "CollectionDeletedResponse", + "CollectionPendingResponse", + "CollectionCreationStatusResponse", + "CollectionDeletionStatusResponse", + "VectorSearchConfiguration", + "VectorSearchDocumentKeyValueListPair", + "VectorSearchFilter", + "TextSearchRequest", + "VectorChunk", + "DocumentOutput", + "DocumentsChunk", + "VectorPerFilterSearchResult", + "VectorSearchResults", + # Clients + "PipelineAPIClient", + "RetrievalAPIClient", + "VectorAPIClient" + +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/document_grounding/client.py b/packages/gen/gen_ai_hub/document_grounding/client.py new file mode 100644 index 0000000..c19afa1 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/client.py @@ -0,0 +1,36 @@ +"""Client module for Document Grounding API. + +This module provides convenient imports for all Document Grounding API clients +and their associated constants. It serves as the main entry point for accessing +Pipeline, Retrieval, and Vector API functionality. + +Exported clients: + - PipelineAPIClient: Client for managing document vectorization pipelines + - RetrievalAPIClient: Client for retrieval operations across data repositories + - VectorAPIClient: Client for vector collection management and semantic search + +Exported constants: + - PATH_DOCUMENT_GROUNDING: Base path for document grounding endpoints + - PATH_DOCUMENT_GROUNDING_PIPELINES: Path for pipeline endpoints + - PATH_DOCUMENT_GROUNDING_RETRIEVAL: Path for retrieval endpoints + - PATH_DOCUMENT_GROUNDING_VECTOR: Path for vector endpoints +""" +from .clients.pipeline_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_PIPELINES, + PATH_DOCUMENT_GROUNDING, + PipelineAPIClient +) +from .clients.retrieval_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_RETRIEVAL, + RetrievalAPIClient +) +from .clients.vector_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_VECTOR, + VectorAPIClient +) + +__all__ = [ + "PipelineAPIClient", + "RetrievalAPIClient", + "VectorAPIClient", +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/document_grounding/clients/__init__.py b/packages/gen/gen_ai_hub/document_grounding/clients/__init__.py new file mode 100644 index 0000000..170de6f --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/clients/__init__.py @@ -0,0 +1,29 @@ +"""Clients subpackage for Document Grounding API. + +This subpackage contains the API client implementations for interacting with +the SAP Generative AI Hub Document Grounding services. + +Available clients: + - PipelineAPIClient: Manages document vectorization pipelines from various data sources + - RetrievalAPIClient: Performs retrieval operations across configured data repositories + - VectorAPIClient: Manages vector collections and performs semantic searches +""" + +from .pipeline_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_PIPELINES, + PATH_DOCUMENT_GROUNDING, + PipelineAPIClient +) +from .retrieval_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_RETRIEVAL, + RetrievalAPIClient +) +from .vector_api_client import ( # pylint: disable=unused-import + PATH_DOCUMENT_GROUNDING_VECTOR, + VectorAPIClient +) + +__all__ = [ + "PipelineAPIClient", "RetrievalAPIClient", "VectorAPIClient", "PATH_DOCUMENT_GROUNDING", + "PATH_DOCUMENT_GROUNDING_VECTOR", "PATH_DOCUMENT_GROUNDING_PIPELINES", "PATH_DOCUMENT_GROUNDING_RETRIEVAL" +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/document_grounding/clients/pipeline_api_client.py b/packages/gen/gen_ai_hub/document_grounding/clients/pipeline_api_client.py new file mode 100644 index 0000000..e2d2dbe --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/clients/pipeline_api_client.py @@ -0,0 +1,330 @@ +"""Pipeline API client for Document Grounding. + +This module provides the PipelineAPIClient class for managing document vectorization +pipelines. Pipelines automate the process of fetching documents from data repositories, +preprocessing and chunking content, generating semantic embeddings, and storing them +in HANA Vector Store. + +Supported data repositories: + - Microsoft SharePoint + - AWS S3 + - SFTP + +API Reference: https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Pipelines +""" +import humps +import requests +from typing import Optional + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubRestClient + +from ..models.pipeline import ( + CreatePipelineRequest, + PipelineIdResponse, + GetPipelinesResponse, + GetPipelineStatusResponse, + BasePipelineResponse, + SearchPipelineRequest, + SearchPipelinesResponse, + GetPipelineExecutionsResponse, + PipelineExecution, + Document, + DocumentsStatusResponse, + ManualPipelineTrigger, +) + +# Constants +PATH_DOCUMENT_GROUNDING = "/lm/document-grounding/pipelines" +PATH_DOCUMENT_GROUNDING_PIPELINES = PATH_DOCUMENT_GROUNDING # PATH_DOCUMENT_GROUNDING is kept for backward compatibility + +class PipelineAPIClient: + """The Pipelines API creates and manages vector stores based on documents from user data repositories: + S3, SFTP, and Microsoft SharePoint. + Each pipeline represents a configured end-to-end process including the following steps: + + - Fetches documents from a supported data source + + - Preprocesses and chunks the document content, and generates semantic embeddings. + Semantic embeddings are multidimensional representations of textual information. + + - Stores semantic embeddings into the HANA Vector Store + + + The Pipeline API is compatible with the following data repositories: + + - Microsoft SharePoint + + - AWS S3 + + - SFTP + + See https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Pipelines + """ + + def __init__( + self, + proxy_client: Optional[GenAIHubProxyClient] = None, + ): + """Initializes the PipelineAPIClient + + :param proxy_client: proxy client to use for requests, defaults to None + :type proxy_client: Optional[GenAIHubProxyClient], optional + """ + + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + self.rest_client = GenAIHubRestClient(self.proxy_client) + self.path = PATH_DOCUMENT_GROUNDING + + def create_pipeline(self, pipeline_request: CreatePipelineRequest) -> PipelineIdResponse: + """Create a document vectorization pipeline + + :param pipeline_request: The object containing the pipeline configuration. + :type pipeline_request: CreatePipelineRequest + :return: ID of the created pipeline + :rtype: PipelineIdResponse + """ + + response = self.rest_client.post(path=self.path, body=pipeline_request.model_dump(exclude_none=True)) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return PipelineIdResponse(**response) + + def get_pipelines(self, top: Optional[int] = None, skip: Optional[int] = None, count: Optional[bool] = None) \ + -> GetPipelinesResponse: + """Get all pipelines. + + :return: Get all pipelines + :rtype: GetPipelinesResponse + """ + + params = {} + if top is not None: + params['$top'] = top + if skip is not None: + params['$skip'] = skip + if count is not None: + params['$count'] = count + response = self.rest_client.get(path=self.path, params=params) + return GetPipelinesResponse(**response) + + def get_pipeline_by_id(self, pipeline_id: str) -> BasePipelineResponse: + """Get details of a pipeline by pipeline id. + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :return: Details of the pipeline + :rtype: BasePipelineResponse + """ + + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}") + return BasePipelineResponse(**response) + + def delete_pipeline_by_id(self, pipeline_id: str) -> requests.Response: + """Delete a pipeline by pipeline id + + :param pipeline_id: ID of the pipeline to delete + :type pipeline_id: str + :return: Response of the delete operation + :rtype: requests.Response + """ + + response = self.rest_client.delete(path=f"{self.path}/{pipeline_id}") + if response == "": # rest_client (ai api sdk) returns empty string for 204 No Content + response = requests.Response() + response.status_code = 204 + return response + + def get_pipeline_status(self, pipeline_id: str) -> GetPipelineStatusResponse: + """Get pipeline status by pipeline id + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :return: Status of the pipeline + :rtype: GetPipelineStatusResponse + """ + + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}/status") + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return GetPipelineStatusResponse(**response) + + def search_pipelines(self, body: SearchPipelineRequest) -> SearchPipelinesResponse: + """Pipeline Search by Metadata + + :param body: The search request object containing metadata filters. + :type body: SearchPipelineRequest + :return: Search results containing matching pipelines. + :rtype: SearchPipelinesResponse + """ + + response = self.rest_client.post(path=f"{self.path}/search", body=body.model_dump(exclude_none=True)) + response = humps.camelize(response) + return SearchPipelinesResponse(**response) + + # pylint: disable=unused-import + def get_pipeline_executions( + self, + pipeline_id: str, + last_execution: Optional[bool] = None, + top: Optional[int] = None, + skip: Optional[int] = None, + count: Optional[bool] = None, + ) -> GetPipelineExecutionsResponse: + """Get Pipeline Executions + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :param last_execution: flag to get only the last execution, defaults to None + :type last_execution: Optional[bool], optional + :param top: number of executions to retrieve, defaults to None + :type top: Optional[int], optional + :param skip: number of executions to skip, defaults to None + :type skip: Optional[int], optional + :param count: flag to include count of total executions, defaults to None + :type count: Optional[bool], optional + :return: Pipeline Executions + :rtype: GetPipelineExecutionsResponse + """ + + params = {} + if last_execution is not None: + params["lastExecution"] = last_execution + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + if count is not None: + params["$count"] = count + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}/executions", params=params) + response = humps.camelize(response) + return GetPipelineExecutionsResponse(**response) + + def get_pipeline_execution_by_id(self, pipeline_id: str, execution_id: str) -> PipelineExecution: + """Get Pipeline Execution by ID + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :param execution_id: Execution ID + :type execution_id: str + :return: Pipeline Execution + :rtype: PipelineExecution + """ + + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}/executions/{execution_id}") + response = humps.camelize(response) + return PipelineExecution(**response) + + # pylint: disable=too-many-arguments + def get_execution_documents( + self, + pipeline_id: str, + execution_id: str, + top: Optional[int] = None, + skip: Optional[int] = None, + count: Optional[bool] = None, + ) -> DocumentsStatusResponse: + """Get Documents for a Pipeline Execution + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :param execution_id: Execution ID + :type execution_id: str + :param top: the maximum number of documents to return, defaults to None + :type top: Optional[int], optional + :param skip: number of documents to skip, defaults to None + :type skip: Optional[int], optional + :param count: flag to include count of total documents, defaults to None + :type count: Optional[bool], optional + :return: Documents for the Pipeline Execution + :rtype: DocumentsStatusResponse + """ + + params = {} + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + if count is not None: + params["$count"] = count + response = self.rest_client.get( + path=f"{self.path}/{pipeline_id}/executions/{execution_id}/documents", + params=params, + ) + response = humps.camelize(response) + return DocumentsStatusResponse(**response) + + def get_execution_document_by_id(self, pipeline_id: str, execution_id: str, document_id: str) -> \ + Document: + """Get Document by ID for a Pipeline Execution + + :return: Document for the Pipeline Execution + :rtype: Document + """ + + response = self.rest_client.get( + path=f"{self.path}/{pipeline_id}/executions/{execution_id}/documents/{document_id}" + ) + response = humps.camelize(response) + return Document(**response) + + def get_pipeline_documents( + self, + pipeline_id: str, + top: Optional[int] = None, + skip: Optional[int] = None, + count: Optional[bool] = None, + ) -> DocumentsStatusResponse: + """Get Documents for a Pipeline + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :param top: the maximum number of documents to return, defaults to None + :type top: Optional[int], optional + :param skip: number of documents to skip, defaults to None + :type skip: Optional[int], optional + :param count: flag to include count of total documents, defaults to None + :type count: Optional[bool], optional + :return: Documents for the Pipeline + :rtype: DocumentsStatusResponse + """ + + params = {} + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + if count is not None: + params["$count"] = count + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}/documents", params=params) + response = humps.camelize(response) + return DocumentsStatusResponse(**response) + + def get_pipeline_document_by_id(self, pipeline_id: str, document_id: str) -> Document: + """Get Document by ID for a Pipeline + + :param pipeline_id: Pipeline ID + :type pipeline_id: str + :param document_id: Document ID + :type document_id: str + :return: Document for the Pipeline + :rtype: Document + """ + + response = self.rest_client.get(path=f"{self.path}/{pipeline_id}/documents/{document_id}") + response = humps.camelize(response) + return Document(**response) + + def trigger_pipeline(self, request: ManualPipelineTrigger) -> requests.Response: + """Trigger Pipeline Manually + + :param request: The manual trigger request object. + :type request: ManualPipelineTrigger + :return: Response of the trigger operation + :rtype: requests.Response + """ + + response = self.rest_client.post(path=f"{self.path}/trigger", body=request.model_dump(exclude_none=True)) + if response == "": # rest_client (ai api sdk) returns empty string for 204 No Content + response = requests.Response() + response.status_code = 202 + return response diff --git a/packages/gen/gen_ai_hub/document_grounding/clients/retrieval_api_client.py b/packages/gen/gen_ai_hub/document_grounding/clients/retrieval_api_client.py new file mode 100644 index 0000000..0216988 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/clients/retrieval_api_client.py @@ -0,0 +1,116 @@ +"""Retrieval API client for Document Grounding. + +This module provides the RetrievalAPIClient class for querying and retrieving +relevant content from configured data repositories. The Retrieval API combines +semantic search with repository metadata filtering and supports custom retrieval +configurations for chunk/document granularity. + +Supported repository types: + - Vector stores + - External document sources (e.g., help.sap.com) + +API Reference: https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Retrieval +""" +import humps +from typing import Optional + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubRestClient + +from ..models.retrieval import ( + DataRepositories, + DataRepository, + RetrievalSearchInput, + RetrievalSearchResults, +) + +# Constants +PATH_DOCUMENT_GROUNDING_RETRIEVAL = "/lm/document-grounding/retrieval" + + +class RetrievalAPIClient: + """ + The Retrieval API enables querying and retrieving relevant content from configured data repositories, + such as vector or external document sources (e.g., help.sap.com). + + Retrieval combines semantic search with repository metadata filtering and supports custom + retrieval configurations for chunk/document granularity. + + Reference: https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Retrieval + """ + + def __init__(self, proxy_client: Optional[GenAIHubProxyClient] = None): + """Initialize the RetrievalAPIClient. + + :param proxy_client: Optional proxy client for making API requests. + :type proxy_client: Optional[GenAIHubProxyClient], optional + """ + + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + self.rest_client = GenAIHubRestClient(self.proxy_client) + self.path = PATH_DOCUMENT_GROUNDING_RETRIEVAL + + # --- Data Repositories --- + + def get_data_repositories( + self, + top: Optional[int] = None, + skip: Optional[int] = None, + count: Optional[bool] = None, + ) -> DataRepositories: + """List all data repositories available to the tenant. + + :param top: the number of items to return, defaults to None + :type top: Optional[int], optional + :param skip: the number of items to skip, defaults to None + :type skip: Optional[int], optional + :param count: whether to include a count of total items, defaults to None + :type count: Optional[bool], optional + :return: DataRepositories model containing the list of data repositories + :rtype: DataRepositories + """ + + params = {} + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + if count is not None: + params["$count"] = count + + response = self.rest_client.get(path=f"{self.path}/dataRepositories", params=params) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return DataRepositories(**response) + + def get_data_repository_by_id(self, repository_id: str) -> DataRepository: + """Get a single data repository by its unique ID. + + :param repository_id: the unique identifier of the data repository + :type repository_id: str + :return: DataRepository model representing the data repository + :rtype: DataRepository + """ + + response = self.rest_client.get(path=f"{self.path}/dataRepositories/{repository_id}") + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return DataRepository(**response) + + # --- Search --- + + def search(self, search_input: RetrievalSearchInput) -> RetrievalSearchResults: + """Perform a retrieval search for relevant content. + + :param search_input: RetrievalSearchInput model defining the query and filters. + :type search_input: RetrievalSearchInput + :return: RetrievalSearchResults model containing repositories, documents, and chunks. + :rtype: RetrievalSearchResults + """ + + response = self.rest_client.post( + path=f"{self.path}/search", + body=search_input.model_dump(exclude_none=True), + ) + + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return RetrievalSearchResults(**response) diff --git a/packages/gen/gen_ai_hub/document_grounding/clients/vector_api_client.py b/packages/gen/gen_ai_hub/document_grounding/clients/vector_api_client.py new file mode 100644 index 0000000..4442824 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/clients/vector_api_client.py @@ -0,0 +1,285 @@ +"""Vector API client for Document Grounding. + +This module provides the VectorAPIClient class for managing vector-based document +collections and performing semantic searches. The Vector API enables creating, +retrieving, updating, and deleting collections, as well as managing documents +within those collections. + +Key capabilities: + - Collection management (create, read, update, delete) + - Document management within collections + - Semantic vector search across collections + - Collection status tracking (creation/deletion) + +API Reference: https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Vector +""" +import humps +import requests +from typing import Optional + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubRestClient +from pydantic import TypeAdapter + +from ..models.vector import ( + CollectionCreateRequest, + Collection, + CollectionsListResponse, + DocumentsCreateRequest, + DocumentsUpdateRequest, + Document, + DocumentsResponse, + DocumentsListResponse, CollectionCreationStatusResponse, CollectionDeletionStatusResponse, TextSearchRequest, + VectorSearchResults, +) + +# Constants +PATH_DOCUMENT_GROUNDING_VECTOR = "/lm/document-grounding/vector" + + +class VectorAPIClient: + """The Vector API provides management and search capabilities for vector-based document collections. + + It enables creating, retrieving, updating, and deleting collections, as well as + managing documents and performing semantic vector searches within those collections. + + Reference: https://api.sap.com/api/DOCUMENT_GROUNDING_API/resource/Vector + """ + + def __init__(self, proxy_client: Optional[GenAIHubProxyClient] = None): + """Initializes the VectorAPIClient + + :param proxy_client: Optional proxy client to use for requests + :type proxy_client: Optional[GenAIHubProxyClient], optional + """ + + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + self.rest_client = GenAIHubRestClient(self.proxy_client) + self.path = PATH_DOCUMENT_GROUNDING_VECTOR + + # --- Collections --- + + def get_collections( + self, top: Optional[int] = None, skip: Optional[int] = None, count: Optional[bool] = None + ) -> CollectionsListResponse: + """Get all collections. + + :param top: the number of collections to retrieve, defaults to None + :type top: Optional[int], optional + :param skip: the number of collections to skip, defaults to None + :type skip: Optional[int], optional + :param count: whether to include the total count of collections, defaults to None + :type count: Optional[bool], optional + :return: A CollectionsListResponse object containing the list of collections + :rtype: CollectionsListResponse + """ + + params = {} + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + if count is not None: + params["$count"] = count + response = self.rest_client.get(path=f"{self.path}/collections", params=params) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return CollectionsListResponse(**response) + + def create_collection(self, collection_request: CollectionCreateRequest) -> requests.Response: + """Create a new collection. + + :param collection_request: The object containing the collection configuration. + :type collection_request: CollectionCreateRequest + :return: requests.Response empty object with 202 status code + :rtype: requests.Response + """ + + response = self.rest_client.post( + path=f"{self.path}/collections", + body=collection_request.model_dump(exclude_none=True) + ) + if response == "": # rest_client (ai api sdk) returns empty string for 202 No Content + response = requests.Response() + response.status_code = 202 + return response + + def get_collection_by_id(self, collection_id: str) -> Collection: + """Get collection details by ID. + + :param collection_id: The ID of the collection to retrieve. + :type collection_id: str + :return: A Collection object containing the collection details + :rtype: Collection + """ + + response = self.rest_client.get(path=f"{self.path}/collections/{collection_id}") + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return Collection(**response) + + def delete_collection(self, collection_id: str) -> requests.Response: + """Delete collection by ID. + + :param collection_id: The ID of the collection to delete. + :type collection_id: str + :return: requests.Response empty object with 204 status code + :rtype: requests.Response + """ + + response = self.rest_client.delete(path=f"{self.path}/collections/{collection_id}") + if response == "": # rest_client (ai api sdk) returns empty string for 204 No Content + response = requests.Response() + response.status_code = 204 + return response + + # --- Documents --- + + def get_documents(self, collection_id: str, top: Optional[int] = None, + skip: Optional[int] = None, count: Optional[bool] = None) -> DocumentsResponse: + """Get documents from a collection. + + :param collection_id: The ID of the collection to retrieve documents from. + :type collection_id: str + :param top: the number of documents to retrieve, defaults to None + :type top: Optional[int], optional + :param skip: the number of documents to skip, defaults to None + :type skip: Optional[int], optional + :param count: whether to include the total count of documents, defaults to None + :type count: Optional[bool], optional + :return: A DocumentsResponse object containing the list of documents + :rtype: DocumentsResponse + """ + + params = {} + if top is not None: + params['$top'] = top + if skip is not None: + params['$skip'] = skip + if count is not None: + params['$count'] = count + response = self.rest_client.get( + path=f"{self.path}/collections/{collection_id}/documents", + params=params + ) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return DocumentsResponse(**response) + + def create_documents(self, collection_id: str, request: DocumentsCreateRequest) -> DocumentsListResponse: + """Create documents in a collection. + + :param collection_id: The ID of the collection to add documents to. + :type collection_id: str + :param request: The object containing the documents to create. + :type request: DocumentsCreateRequest + :return: A DocumentsListResponse object containing the created documents + :rtype: DocumentsListResponse + """ + + response = self.rest_client.post( + path=f"{self.path}/collections/{collection_id}/documents", + body=request.model_dump(exclude_none=True) + ) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return DocumentsListResponse(**response) + + def update_documents(self, collection_id: str, request: DocumentsUpdateRequest) -> DocumentsListResponse: + """Update documents in a collection. + + :param collection_id: The ID of the collection to update documents in. + :type collection_id: str + :param request: The object containing the documents to update. + :type request: DocumentsUpdateRequest + :return: A DocumentsListResponse object containing the updated documents + :rtype: DocumentsListResponse + """ + + response = self.rest_client.patch( + path=f"{self.path}/collections/{collection_id}/documents", + body=request.model_dump(exclude_none=True) + ) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return DocumentsListResponse(**response) + + def get_document_by_id(self, collection_id: str, document_id: str) -> Document: + """Get a document by ID from a collection. + + :param collection_id: The ID of the collection to retrieve the document from. + :type collection_id: str + :param document_id: The ID of the document to retrieve. + :type document_id: str + :return: A Document object containing the document details + :rtype: Document + """ + + response = self.rest_client.get( + path=f"{self.path}/collections/{collection_id}/documents/{document_id}" + ) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return Document(**response) + + def delete_document(self, collection_id: str, document_id: str) -> requests.Response: + """Delete a document from a collection. + + :param collection_id: The ID of the collection to delete the document from. + :type collection_id: str + :param document_id: The ID of the document to delete. + :type document_id: str + :return: requests.Response empty object with 204 status code + :rtype: requests.Response + """ + + response = self.rest_client.delete( + path=f"{self.path}/collections/{collection_id}/documents/{document_id}" + ) + if response == "": # 204 No Content + response = requests.Response() + response.status_code = 204 + return response + + # --- Collection statuses --- + + def get_collection_creation_status(self, collection_id: str) -> CollectionCreationStatusResponse: + """Get creation status for a collection. + + :param collection_id: The ID of the collection to retrieve the creation status for. + :type collection_id: str + :return: A CollectionCreationStatusResponse object containing the creation status + :rtype: CollectionCreationStatusResponse + """ + + response = self.rest_client.get(path=f"{self.path}/collections/{collection_id}/creationStatus") + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + adapter = TypeAdapter(CollectionCreationStatusResponse) + return adapter.validate_python(response) + + def get_collection_deletion_status(self, collection_id: str) -> CollectionDeletionStatusResponse: + """Get deletion status for a collection. + + :param collection_id: The ID of the collection to retrieve the deletion status for. + :type collection_id: str + :return: A CollectionDeletionStatusResponse object containing the deletion status + :rtype: CollectionDeletionStatusResponse + """ + + response = self.rest_client.get(path=f"{self.path}/collections/{collection_id}/deletionStatus") + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + adapter = TypeAdapter(CollectionDeletionStatusResponse) + return adapter.validate_python(response) + + # --- Search --- + + def search(self, request: TextSearchRequest) -> VectorSearchResults: + """Perform semantic search in vector collections. + + :param request: The object containing the search parameters. + :type request: TextSearchRequest + :return: A VectorSearchResults object containing the search results + :rtype: VectorSearchResults + """ + + response = self.rest_client.post( + path=f"{self.path}/search", + body=request.model_dump(exclude_none=True) + ) + response = humps.camelize(response) # rest_client (ai api sdk) returns snake_case responses + return VectorSearchResults(**response) diff --git a/packages/gen/gen_ai_hub/document_grounding/models/__init__.py b/packages/gen/gen_ai_hub/document_grounding/models/__init__.py new file mode 100644 index 0000000..b53a093 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/models/__init__.py @@ -0,0 +1,172 @@ +"""Models subpackage for Document Grounding API. + +This subpackage contains Pydantic model definitions for all Document Grounding +API requests and responses. Models are organized by API domain: + +- pipeline: Models for Pipeline API (document vectorization pipelines) +- retrieval: Models for Retrieval API (content retrieval from repositories) +- vector: Models for Vector API (vector collection management and search) + +These models provide type-safe data structures for interacting with the +Document Grounding APIs and ensure proper validation of request/response data. +""" +from .pipeline import ( + CreatePipelineRequest, + MSSharePointPipelineCreateRequest, + S3PipelineCreateRequest, + SFTPPipelineCreateRequest, + SearchPipelineRequest, + DataRepositoryMetadataItem, + CommonConfiguration, + MetaData, + MSSharePointConfiguration, + SharePointConfig, + SharePointSite, + ManualPipelineTrigger, + PipelineIdResponse, + GetPipelineResponse, + GetPipelinesResponse, + GetPipelineStatusResponse, + PipelineExecution, + GetPipelineExecutionsResponse, + Document, + DocumentsStatusResponse, + MSSharePointPipelineGetResponse, + S3PipelineGetResponse, + SFTPPipelineGetResponse, + SearchPipelineData, + SearchPipelinesResponse, + PipelineExecutionStatus, + DocumentStatus, + BasePipelineResponse, + MSSharePointConfigurationGetResponse +) + +from .retrieval import ( + RetrievalKeyValueListPair, + RetrievalDocumentKeyValueListPair, + RetrievalSearchDocumentKeyValueListPair, + RetrievalChunk, + RetrievalDocument, + DataRepositoryType, + DataRepository, + DataRepositoryWithDocuments, + RetrievalSearchConfiguration, + RetrievalSearchFilter, + RetrievalSearchInput, + RetrievalDataRepositorySearchResult, + RetrievalPerFilterSearchResult, + RetrievalPerFilterSearchResultError, + RetrievalPerFilterSearchResultWithError, + RetrievalSearchResults, + DataRepositories +) + +from .vector import ( + VectorKeyValueListPair, + EmbeddingConfig, + CollectionCreateRequest, + Collection, + CollectionsListResponse, + TextOnlyBaseChunk, + BaseDocument, + DocumentWithoutChunks, + Document as VectorDocument, + DocumentsCreateRequest, + DocumentsUpdateRequest, + DocumentsListResponse, + DocumentsResponse, + CollectionCreatedResponse, + CollectionDeletedResponse, + CollectionPendingResponse, + CollectionCreationStatusResponse, + CollectionDeletionStatusResponse, + VectorSearchConfiguration, + VectorSearchDocumentKeyValueListPair, + VectorSearchFilter, + TextSearchRequest, + VectorChunk, + DocumentOutput, + DocumentsChunk, + VectorPerFilterSearchResult, + VectorSearchResults +) + +__all__ = [ + # Pipeline models + "CreatePipelineRequest", + "MSSharePointPipelineCreateRequest", + "S3PipelineCreateRequest", + "SFTPPipelineCreateRequest", + "SearchPipelineRequest", + "DataRepositoryMetadataItem", + "CommonConfiguration", + "MetaData", + "MSSharePointConfiguration", + "SharePointConfig", + "SharePointSite", + "ManualPipelineTrigger", + "PipelineIdResponse", + "GetPipelineResponse", + "GetPipelinesResponse", + "GetPipelineStatusResponse", + "PipelineExecution", + "GetPipelineExecutionsResponse", + "Document", + "DocumentsStatusResponse", + "MSSharePointPipelineGetResponse", + "S3PipelineGetResponse", + "SFTPPipelineGetResponse", + "SearchPipelineData", + "SearchPipelinesResponse", + "PipelineExecutionStatus", + "DocumentStatus", + "BasePipelineResponse", + "MSSharePointConfigurationGetResponse", + # Retrieval models + "RetrievalKeyValueListPair", + "RetrievalDocumentKeyValueListPair", + "RetrievalSearchDocumentKeyValueListPair", + "RetrievalChunk", + "RetrievalDocument", + "DataRepositoryType", + "DataRepository", + "DataRepositoryWithDocuments", + "RetrievalSearchConfiguration", + "RetrievalSearchFilter", + "RetrievalSearchInput", + "RetrievalDataRepositorySearchResult", + "RetrievalPerFilterSearchResult", + "RetrievalPerFilterSearchResultError", + "RetrievalPerFilterSearchResultWithError", + "RetrievalSearchResults", + "DataRepositories", + # Vector models + "VectorKeyValueListPair", + "EmbeddingConfig", + "CollectionCreateRequest", + "Collection", + "CollectionsListResponse", + "TextOnlyBaseChunk", + "BaseDocument", + "DocumentWithoutChunks", + "VectorDocument", + "DocumentsCreateRequest", + "DocumentsUpdateRequest", + "DocumentsListResponse", + "DocumentsResponse", + "CollectionCreatedResponse", + "CollectionDeletedResponse", + "CollectionPendingResponse", + "CollectionCreationStatusResponse", + "CollectionDeletionStatusResponse", + "VectorSearchConfiguration", + "VectorSearchDocumentKeyValueListPair", + "VectorSearchFilter", + "TextSearchRequest", + "VectorChunk", + "DocumentOutput", + "DocumentsChunk", + "VectorPerFilterSearchResult", + "VectorSearchResults" +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/document_grounding/models/pipeline.py b/packages/gen/gen_ai_hub/document_grounding/models/pipeline.py new file mode 100644 index 0000000..4600827 --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/models/pipeline.py @@ -0,0 +1,171 @@ +"""Pydantic models for Pipeline API. + +This module defines data models for the Pipeline API, which manages document +vectorization pipelines from various data sources (Microsoft SharePoint, AWS S3, SFTP). + +Model categories: + - Pipeline configuration models (create/get requests and responses) + - Pipeline execution models (tracking pipeline runs) + - Document models (tracking document processing status) + - Search and metadata models (filtering pipelines by metadata) + - Trigger models (manual pipeline execution) + +All models use Pydantic for validation and serialization. +""" +from typing import List, Optional, Union, Annotated, Literal +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +# --- Models for Pipeline API --- + +class MetaData(BaseModel): + destination: str + +class SharePointSite(BaseModel): + name: str + includePaths: Optional[List[str]] = None + +class SharePointConfig(BaseModel): + site: SharePointSite + +class MSSharePointConfiguration(BaseModel): + destination: str + sharePoint: SharePointConfig + +class CommonConfiguration(BaseModel): + destination: str + +class MSSharePointPipelineCreateRequest(BaseModel): + type: Literal["MSSharePoint"] = "MSSharePoint" + configuration: MSSharePointConfiguration + metadata: Optional[MetaData] = None + +class S3PipelineCreateRequest(BaseModel): + type: Literal["S3"] = "S3" + configuration: CommonConfiguration + metadata: Optional[MetaData] = None + +class SFTPPipelineCreateRequest(BaseModel): + type: Literal["SFTP"] = "SFTP" + configuration: CommonConfiguration + metadata: Optional[MetaData] = None + +CreatePipelineRequest = Union[ + MSSharePointPipelineCreateRequest, + S3PipelineCreateRequest, + SFTPPipelineCreateRequest +] + +class PipelineIdResponse(BaseModel): + pipelineId: str + +class BasePipelineResponse(BaseModel): + id: str + type: str + metadata: Optional[MetaData] = None + +class MSSharePointConfigurationGetResponse(BaseModel): + destination: str + sharePoint: SharePointConfig + +class MSSharePointPipelineGetResponse(BasePipelineResponse): + type: Literal["MSSharePoint"] = "MSSharePoint" + configuration: MSSharePointConfigurationGetResponse + +class S3PipelineGetResponse(BasePipelineResponse): + type: Literal["S3"] = "S3" + configuration: CommonConfiguration + +class SFTPPipelineGetResponse(BasePipelineResponse): + type: Literal["SFTP"] = "SFTP" + configuration: CommonConfiguration + +GetPipelineResponse = Annotated[ + MSSharePointPipelineGetResponse | S3PipelineGetResponse | SFTPPipelineGetResponse, + Field(discriminator="type") +] + +class GetPipelinesResponse(BaseModel): + count: Optional[int] + resources: List[GetPipelineResponse] + +class GetPipelineStatusResponse(BaseModel): + lastStarted: Optional[str] + status: Optional[str] + +# --- Search --- + +class DataRepositoryMetadataItem(BaseModel): + key: str + value: List[str] + + +class SearchPipelineRequest(BaseModel): + dataRepositoryMetadata: List[DataRepositoryMetadataItem] + + +class SearchPipelineData(BaseModel): + pipelineId: str + + +class SearchPipelinesResponse(BaseModel): + count: Optional[int] + resources: List[SearchPipelineData] + + +# --- Executions (Pipeline Runs) --- + +class PipelineExecutionStatus(str, Enum): + NEW = "NEW" + UNKNOWN = "UNKNOWN" + INPROGRESS = "INPROGRESS" + FINISHED = "FINISHED" + FINISHED_WITH_ERRORS = "FINISHEDWITHERRORS" + TIMEOUT = "TIMEOUT" + +class PipelineExecution(BaseModel): + id: str + status: Optional[PipelineExecutionStatus] = None + createdAt: Optional[datetime] = None + modifiedAt: Optional[datetime] = None + + +class GetPipelineExecutionsResponse(BaseModel): + count: Optional[int] + resources: List[PipelineExecution] + + +# --- Documents --- + +class DocumentStatus(str, Enum): + TO_BE_PROCESSED = "TO_BE_PROCESSED" + INDEXED = "INDEXED" + REINDEXED = "REINDEXED" + DEINDEXED = "DEINDEXED" + FAILED = "FAILED" + FAILED_TO_BE_RETRIED = "FAILED_TO_BE_RETRIED" + TO_BE_SCHEDULED = "TO_BE_SCHEDULED" + +class Document(BaseModel): + id: str + status: Optional[DocumentStatus] = None + viewLocation: Optional[str] = None + downloadLocation: Optional[str] = None + absoluteUrl: Optional[str] = None + title: Optional[str] = None + metadataId: Optional[str] = None + createdTimestamp: Optional[datetime] = None + lastUpdatedTimestamp: Optional[datetime] = None + + +class DocumentsStatusResponse(BaseModel): + count: Optional[int] + resources: List[Document] + + +# --- Trigger --- + +class ManualPipelineTrigger(BaseModel): + pipelineId: str + metadataOnly: Optional[bool] = None diff --git a/packages/gen/gen_ai_hub/document_grounding/models/retrieval.py b/packages/gen/gen_ai_hub/document_grounding/models/retrieval.py new file mode 100644 index 0000000..fced01b --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/models/retrieval.py @@ -0,0 +1,125 @@ +"""Pydantic models for Retrieval API. + +This module defines data models for the Retrieval API, which enables querying +and retrieving relevant content from configured data repositories (vector stores +and external document sources). + +Model categories: + - Data repository models (repository information and metadata) + - Chunk and document models (content structure) + - Search filter and configuration models (query parameters) + - Search input and result models (request/response structures) + +The Retrieval API supports semantic search combined with metadata filtering +for precise content retrieval across multiple repository types. +""" +from typing import List, Optional, Union, Literal +from pydantic import BaseModel, Field + + +# --- Common Key/Value models --- + +class RetrievalKeyValueListPair(BaseModel): + key: str + value: List[str] + + +class RetrievalDocumentKeyValueListPair(RetrievalKeyValueListPair): + matchMode: Optional[str] + + + +class RetrievalSearchDocumentKeyValueListPair(BaseModel): + key: str + value: List[str] + selectMode: Optional[List[str]] = None + + +# --- Retrieval Chunk and Document models --- + +class RetrievalChunk(BaseModel): + id: str + content: str + metadata: Optional[List[RetrievalKeyValueListPair]] = Field(default_factory=list) + + +class RetrievalDocument(BaseModel): + id: str + metadata: Optional[List[RetrievalDocumentKeyValueListPair]] = Field(default_factory=list) + chunks: List[RetrievalChunk] + + +# --- Data Repository models --- + +DataRepositoryType = Union[ + Literal["vector", "help.sap.com"], + str +] + + +class DataRepository(BaseModel): + id: str + title: str + type: DataRepositoryType + metadata: Optional[List[RetrievalKeyValueListPair]] = Field(default_factory=list) + + +class DataRepositoryWithDocuments(BaseModel): + id: str + title: str + metadata: Optional[List[RetrievalKeyValueListPair]] = Field(default_factory=list) + documents: List[RetrievalDocument] + + +# --- Retrieval Filter and Configuration models --- + +class RetrievalSearchConfiguration(BaseModel): + maxChunkCount: Optional[int] = None + maxDocumentCount: Optional[int] = None + + +class RetrievalSearchFilter(BaseModel): + id: str + dataRepositoryType: DataRepositoryType + searchConfiguration: Optional[RetrievalSearchConfiguration] = Field(default_factory=RetrievalSearchConfiguration) + dataRepositories: Optional[List[str]] = Field(default_factory=list) + dataRepositoryMetadata: Optional[List[RetrievalKeyValueListPair]] = Field(default_factory=list) + documentMetadata: Optional[List[RetrievalSearchDocumentKeyValueListPair]] = Field(default_factory=list) + chunkMetadata: Optional[List[RetrievalKeyValueListPair]] = Field(default_factory=list) + + +# --- Retrieval Search Input and Results --- + +class RetrievalSearchInput(BaseModel): + query: str + filters: List[RetrievalSearchFilter] + + +class RetrievalDataRepositorySearchResult(BaseModel): + dataRepository: DataRepositoryWithDocuments + + +class RetrievalPerFilterSearchResult(BaseModel): + filterId: str + results: List[RetrievalDataRepositorySearchResult] = Field(default_factory=list) + + +class RetrievalPerFilterSearchResultError(BaseModel): + message: str + + +class RetrievalPerFilterSearchResultWithError(BaseModel): + filterId: str + error: RetrievalPerFilterSearchResultError + + +class RetrievalSearchResults(BaseModel): + results: List[Union[RetrievalPerFilterSearchResult, RetrievalPerFilterSearchResultWithError]] + + +# --- List & Single Repository responses --- + +class DataRepositories(BaseModel): + count: Optional[int] = None + resources: List[DataRepository] + diff --git a/packages/gen/gen_ai_hub/document_grounding/models/vector.py b/packages/gen/gen_ai_hub/document_grounding/models/vector.py new file mode 100644 index 0000000..9f4c0cd --- /dev/null +++ b/packages/gen/gen_ai_hub/document_grounding/models/vector.py @@ -0,0 +1,168 @@ +"""Pydantic models for Vector API. + +This module defines data models for the Vector API, which provides management +and search capabilities for vector-based document collections. + +Model categories: + - Collection models (collection configuration and management) + - Document and chunk models (content structure with embeddings) + - Embedding configuration models (embedding model settings) + - Search models (semantic search requests and results) + - Status models (collection creation/deletion tracking) + +The Vector API enables semantic search across document collections using +vector embeddings for similarity-based retrieval. +""" +from typing import List, Optional, Annotated, Literal +from pydantic import BaseModel, Field + + +# --- Models for Vector API --- + +# --- Common key-value metadata pair --- +class VectorKeyValueListPair(BaseModel): + key: str + value: List[str] + + +# --- Embedding Config --- +class EmbeddingConfig(BaseModel): + modelName: Optional[str] = Field(default="text-embedding-3-large") + + +# --- Collection Models --- +class CollectionCreateRequest(BaseModel): + title: Optional[str] = None + embeddingConfig: EmbeddingConfig + metadata: Optional[List[VectorKeyValueListPair]] = [] + + +class Collection(BaseModel): + id: str + title: Optional[str] = None + embeddingConfig: EmbeddingConfig + metadata: Optional[List[VectorKeyValueListPair]] = [] + + +class CollectionsListResponse(BaseModel): + count: Optional[int] = None + resources: List[Collection] + + +# --- Chunk and Document Models --- +class TextOnlyBaseChunk(BaseModel): + content: str + metadata: Optional[List[VectorKeyValueListPair]] = [] + + +class BaseDocument(BaseModel): + chunks: List[TextOnlyBaseChunk] + metadata: List[VectorKeyValueListPair] + + +class DocumentWithoutChunks(BaseModel): + id: str + metadata: List[VectorKeyValueListPair] + + +class Document(BaseDocument): + id: str + + +class DocumentsCreateRequest(BaseModel): + documents: List[BaseDocument] + + +class DocumentsUpdateRequest(BaseModel): + documents: List[Document] + + +class DocumentsListResponse(BaseModel): + documents: List[DocumentWithoutChunks] + + +class DocumentsResponse(BaseModel): + count: Optional[int] = None + resources: List[DocumentWithoutChunks] + + +# --- Collection Status Models --- + +class CollectionCreatedResponse(BaseModel): + collectionURL: str = Field(alias="collectionUrl") + status: Literal["CREATED"] = "CREATED" + + +class CollectionDeletedResponse(BaseModel): + collectionURL: str = Field(alias="collectionUrl") + status: Literal["DELETED"] = "DELETED" + + +class CollectionPendingResponse(BaseModel): + Location: str = Field(alias="location") + status: Literal["PENDING"] = "PENDING" + + +CollectionCreationStatusResponse = Annotated[ + CollectionCreatedResponse | CollectionPendingResponse, + Field(discriminator="status") +] + +CollectionDeletionStatusResponse = Annotated[ + CollectionDeletedResponse | CollectionPendingResponse, + Field(discriminator="status") +] + +# --- Vector Search Models --- +class VectorSearchConfiguration(BaseModel): + maxChunkCount: Optional[int] = None + maxDocumentCount: Optional[int] = None + + +class VectorSearchDocumentKeyValueListPair(BaseModel): + key: str + value: List[str] + selectMode: Optional[List[str]] = None + + +class VectorSearchFilter(BaseModel): + id: str + collectionIds: List[str] + configuration: VectorSearchConfiguration + collectionMetadata: Optional[List[VectorKeyValueListPair]] = [] + documentMetadata: Optional[List[VectorSearchDocumentKeyValueListPair]] = [] + chunkMetadata: Optional[List[VectorKeyValueListPair]] = [] + + +class TextSearchRequest(BaseModel): + query: str + filters: List[VectorSearchFilter] + + +# --- Vector Search Results --- +class VectorChunk(BaseModel): + id: str + content: str + metadata: Optional[List[VectorKeyValueListPair]] = [] + + +class DocumentOutput(BaseModel): + id: str + metadata: Optional[List[VectorKeyValueListPair]] = [] + chunks: List[VectorChunk] + + +class DocumentsChunk(BaseModel): + id: str + title: str + metadata: Optional[List[VectorKeyValueListPair]] = [] + documents: List[DocumentOutput] + + +class VectorPerFilterSearchResult(BaseModel): + filterId: str + results: List[DocumentsChunk] + + +class VectorSearchResults(BaseModel): + results: List[VectorPerFilterSearchResult] diff --git a/packages/gen/gen_ai_hub/evaluations/__init__.py b/packages/gen/gen_ai_hub/evaluations/__init__.py new file mode 100644 index 0000000..61fef00 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/__init__.py @@ -0,0 +1,6 @@ +from .models import Dataset, EvaluationConfig, MetricConfig, MetricRef, ArtifactSource, Results, EvaluationRun +from .client import EvaluationClient + + +__all__ = ['EvaluationClient', "Dataset", "EvaluationConfig", "MetricConfig", "MetricRef", "ArtifactSource", + 'EvaluationRun', 'Results'] diff --git a/packages/gen/gen_ai_hub/evaluations/_internal/__init__.py b/packages/gen/gen_ai_hub/evaluations/_internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/evaluations/_internal/_models.py b/packages/gen/gen_ai_hub/evaluations/_internal/_models.py new file mode 100644 index 0000000..64c212c --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/_internal/_models.py @@ -0,0 +1,69 @@ +from typing import List, Any, Optional, Union +from dataclasses import dataclass, field + +@dataclass +class _EvaluationConfigData: + """ + Defines the helper model to store the data of orch_configs, testdataset, custom_metric_config in case of user not providing the direct config. + + :param orch_config_data: Orchestration configuration data. + Can be a single dictionary or a list of dictionaries. + :type orch_config_data: Union[dict, List[dict]] + + :param dataset_type: Type of dataset (e.g., ``json``, ``jsonl``, ``csv``). + :type dataset_type: str + + :param dataset_data: Loaded dataset content. Supports multiple formats + depending on ``dataset_type``. + :type dataset_data: Any + + :param metric_templates: List of custom metric template configurations. + :type metric_templates: List[dict] + + :param metrics_list: List of metric identifiers to evaluate. + :type metrics_list: List[str] + + :param variable_mapping: Optional mapping between prompt/metric variables + and dataset columns. + :type variable_mapping: Optional[dict] + + :param test_row_count: Number of dataset rows to evaluate. Defaults to ``-1`` (all rows). + :type test_row_count: Optional[int] + + :param repetitions: Number of times each test should be repeated. Defaults to ``1``. + :type repetitions: Optional[int] + + :param tags: Optional metadata tags associated with the evaluation run. + :type tags: Optional[dict] + + :param debug_mode: Enables debug mode for additional logging or diagnostics. + :type debug_mode: Optional[bool] + """ + + orch_config_data: Union[dict | List[dict]] + dataset_type: str + dataset_data: Any # to support for csv,json,jsonl data types + metric_templates: List[dict] + metrics_list: List[str] + variable_mapping: Optional[dict] = ( + None # after converting the Json dict to string to be compatible with the utils code + ) + test_row_count: Optional[int] = -1 + repetitions: Optional[int] = 1 + tags: Optional[dict] = field(default_factory=dict) + debug_mode: Optional[bool] = False + + +@dataclass +class _AWSObjectStoreData: + """ + Stores AWS object store credentials used for accessing S3-compatible storage. + + :param aws_access_key_id: AWS access key ID. + :type aws_access_key_id: str + + :param aws_secret_access_key: AWS secret access key. + :type aws_secret_access_key: str + """ + aws_access_key_id: str + aws_secret_access_key: str diff --git a/packages/gen/gen_ai_hub/evaluations/client.py b/packages/gen/gen_ai_hub/evaluations/client.py new file mode 100644 index 0000000..59b399b --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/client.py @@ -0,0 +1,568 @@ +from typing import List +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_api_client_sdk.models.model import Model +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.constants import ( + DEFAULT_KEY, + AWS_PROVIDER_KEY, + INPUT_SECRET_SETUP_KEY, + DEFAULT_SECRET_SETUP_KEY, + ORCHESTRATION_URL_SETUP_KEY, + AWS_S3_OSS_TYPE_KEY, + SUPPORTED_OSS_TYPES, + OBJECT_STORE_SECRET_EXISTS_MESSAGE, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, +) +from gen_ai_hub.evaluations.utils.oss_secret_utils import ( + create_aws_object_store_secret, + fetch_object_store_secret_by_name, + delete_object_store_secret, +) +from gen_ai_hub.evaluations.utils.aicore_utils import ( + create_llm_orchestration_deployment_url, + list_available_llm_models, +) +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations._internal._models import ( + _EvaluationConfigData, + _AWSObjectStoreData, +) +from gen_ai_hub.evaluations.helpers.config_data import ( + extract_config_data, + build_accumulated_config, +) +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.evaluations.utils.validation_utils import ( + validate_config_data_collection, + validate_orchestration_url_across_configs, +) +from gen_ai_hub.evaluations.credentials import fetch_credentials +from gen_ai_hub.evaluations.models.evaluation_run import EvaluationRun +from gen_ai_hub.evaluations.helpers.evaluation_optimization_flow import ( + single_evaluation_job_flow, + multiple_evaluation_jobs_flow, +) + +from gen_ai_hub.evaluations.utils.orch_config_utils import ( + validate_orchestration_params_from_evaluation_config, +) + +from gen_ai_hub.evaluations.utils.metric_client_utils import ( + fetch_all_system_predefined_metrics, +) + +from gen_ai_hub.evaluations.helpers.logging import get_logger +from gen_ai_hub.orchestration_v2.service import get_orchestration_api_url + +logger = get_logger() + + +def _has_mixed_config_types(evaluation_configs: List[EvaluationConfig]) -> bool: + """Check if evaluation configs have mixed types (llm+template and orchestration_registry). + + A single execution cannot handle both types together, so this detection is used + to determine if multiple executions are required. + + :param evaluation_configs: List of evaluation configuration objects + :type evaluation_configs: List[EvaluationConfig] + :return: True if configs contain both llm and orchestration_registry types + :rtype: bool + """ + if len(evaluation_configs) <= 1: + return False + + has_llm = any(config.llm is not None for config in evaluation_configs) + has_registry = any(config.orchestration_registry_reference is not None for config in evaluation_configs) + + return has_llm and has_registry + + +class EvaluationClient: + """ + Base Client for the Evaluations service + """ + + def __init__( + self, + base_url: str, + auth_url: str = None, + client_id: str = None, + client_secret: str = None, + cert_str: str = None, + key_str: str = None, + cert_file_path: str = None, + key_file_path: str = None, + resource_group: str = None, + aws_access_key_id: str = None, + aws_secret_access_key: str = None, + ai_core_client: AICoreV2Client = None, + orchestration_url: str = None, + input_object_store_secret_name: str = None, + provider_name: str = AWS_PROVIDER_KEY, # later will be a mandatory param with no default value + ): + """ + EvaluationsClient root object to be used for Evaluations. + + :param base_url: Base URL of the AI Core instance (must include `/v2` suffix). + :type base_url: str + :param auth_url: Authentication URL used to retrieve access tokens. + :type auth_url: str, optional + :param client_id: OAuth client ID. + :type client_id: str, optional + :param client_secret: OAuth client secret. + :type client_secret: str, optional + :param cert_str: X.509 certificate content as a string. + :type cert_str: str, optional + :param key_str: X.509 private key content as a string. + :type key_str: str, optional + :param cert_file_path: File path to X.509 certificate. + :type cert_file_path: str, optional + :param key_file_path: File path to X.509 private key. + :type key_file_path: str, optional + :param resource_group: Resource group name within the AI Core instance. + :type resource_group: str, optional + :param aws_access_key_id: AWS access key ID. + :type aws_access_key_id: str, optional + :param aws_secret_access_key: AWS secret access key. + :type aws_secret_access_key: str, optional + :param ai_core_client: Pre-configured AI Core client instance. + :type ai_core_client: AICoreV2Client, optional + :param orchestration_url: Pre-existing orchestration deployment URL. + :type orchestration_url: str, optional + :param input_object_store_secret_name: Name of input object store secret. + :type input_object_store_secret_name: str, optional + :param provider_name: Hyperscaler provider name (e.g., "aws"). + :type provider_name: str, optional + :raises ValueError: If required hyperscaler provider parameters are missing. + """ + + logger.info("Initializing the Evaluations client") + self.base_url = base_url + self.resource_group = resource_group + self.ai_core_client = ( + ai_core_client + if ai_core_client is not None + else AICoreV2Client( + base_url=base_url, + auth_url=auth_url, + client_id=client_id, + client_secret=client_secret, + cert_str=cert_str, + key_str=key_str, + cert_file_path=cert_file_path, + key_file_path=key_file_path, + resource_group=resource_group, + ) + ) + + self.__gen_ai_hub_proxy_client = get_proxy_client( + proxy_version="gen-ai-hub", + base_url=base_url, + auth_url=auth_url, + client_id=client_id, + client_secret=client_secret, + resource_group=resource_group, + ) + self.__gen_ai_hub_proxy_client.ai_core_client = self.ai_core_client + + self.aws_access_key_id = aws_access_key_id + self.aws_secret_access_key = aws_secret_access_key + self.orchestration_url = orchestration_url + self.user_provided_orchestration_url = ( + True if self.orchestration_url is not None else False + ) # using this flag to figure out whether url has been passed from user to only validate in that case + self.default_object_store_secret_name = None + self.input_object_store_secret_name = input_object_store_secret_name + self.provider_name = provider_name + logger.info("Validating whether all the required params have been passed") + self._validate_provider_params() + logger.info("Initialization of the client completed!") + + def __repr__(self): + attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items()) + return f"{self.__class__.__name__}({attrs})" + + def _validate_provider_params(self): + """ + Validate required hyperscaler provider parameters. + :raises ValueError: If required provider parameters are missing. + """ + # params related to aicore will fail if not provided when tried to initialise the AICOREV2 client + errors = [] + provider_name = self.provider_name + required_provider_params = [] + if provider_name == AWS_PROVIDER_KEY: + required_provider_params = { + AWS_ACCESS_KEY_ID.lower(): self.aws_access_key_id, + AWS_SECRET_ACCESS_KEY.lower(): self.aws_secret_access_key, + } + # add support for other providers + + for key, value in required_provider_params.items(): + if value is None: + errors.append(key) + + if errors: + raise ValueError( + f"Missing required hyperscalar provider params: {', '.join(errors)}" + ) + + @staticmethod + def from_env(profile_name: str = None, **kwargs): + """ + Alternative way to create an EvaluationClient object. + + Parameter resolution precedence: + 1. Explicit keyword arguments + 2. Environment variables + 3. Configuration file + 4. VCAP_SERVICES environment variable + + :param profile_name: Profile name defined in configuration. + :type profile_name: str, optional + :param kwargs: Additional parameters passed to constructor. + :return: Configured EvaluationClient instance. + :rtype: EvaluationClient + """ + env_credentials = fetch_credentials(profile=profile_name, **kwargs) + + # if cert_url is present in the fetched credentials, rename it to auth_url + if "cert_url" in env_credentials: + env_credentials["auth_url"] = env_credentials.pop("cert_url") + + kwargs.update(env_credentials) + return EvaluationClient(**kwargs) + + + def validate_secret_type(self, secret_type: str, creator_mapping: dict): + if secret_type not in creator_mapping: + raise ValueError( + f"Invalid object store secret type, please use one among the supported types: {SUPPORTED_OSS_TYPES}" + ) + + + def create_or_update_object_store_secret( + self, + *, + context, + secret_body: dict, + is_default: bool, + result_key: str, + attr_name: str, + creator_mapping: dict, + replace_existing: bool, + result: dict, + ): + secret_type = secret_body.get("type") or AWS_S3_OSS_TYPE_KEY + secret_name = secret_body.get("name") or DEFAULT_KEY + + self.validate_secret_type(secret_type, creator_mapping) + + try: + response = creator_mapping[secret_type]( + context.aws_access_key_id, + context.aws_secret_access_key, + context.ai_core_client, + context.resource_group, + secret_body, + is_default, + ) + + if response.message.lower() == OBJECT_STORE_SECRET_EXISTS_MESSAGE.lower(): + if not replace_existing: + raise ValueError( + f"{secret_name} Object store secret already exists. " + "Use replace_existing=True to overwrite." + ) + + # delete and recreate + delete_object_store_secret( + context.ai_core_client, + secret_name, + context.resource_group, + ) + + return self.create_or_update_object_store_secret( + context=context, + secret_body=secret_body, + is_default=is_default, + result_key=result_key, + attr_name=attr_name, + creator_mapping=creator_mapping, + replace_existing=False, + result=result, + ) + + except Exception as e: + logger.exception(e) + raise RuntimeError( + f"Creation of {secret_name} object store secret failed with error: {e}" + ) from e + + setattr(context, attr_name, secret_name) + result[result_key] = secret_name + + + def resolve_orchestration_deployment_url(self) -> str: + """ + Resolves the orchestration deployment URL. + + For non-default resource groups, creates a new deployment. + For default resource group, attempts to discover existing deployment + with the default config name using the orchestration service, + or creates one if not found. + + :return: The orchestration deployment URL. + :rtype: str + """ + # For non-default resource groups, create deployment directly + if self.resource_group != DEFAULT_KEY: + return create_llm_orchestration_deployment_url( + self.ai_core_client, + self.resource_group, + ) + + # For default resource group, try to discover existing deployment + # using the orchestration service's get_orchestration_api_url function + try: + orchestration_url = get_orchestration_api_url( + self.__gen_ai_hub_proxy_client, + ) + logger.info("Found existing orchestration deployment: %s", orchestration_url) + return orchestration_url + except ValueError: + # No deployment found, create a new one + logger.info("No existing orchestration deployment found, creating new deployment") + return create_llm_orchestration_deployment_url( + self.ai_core_client, + self.resource_group, + ) + + + def setup( + self, + input_secret_body: dict | None = None, + default_secret_body: dict | None = None, + replace_existing: bool = False, + ): + """ + One time setup function which does object store secrets creation + and orchestration deployment url creation if not provided. + """ + + secret_creator_mapping = { + AWS_S3_OSS_TYPE_KEY: create_aws_object_store_secret, + } + + result: dict = {} + + if input_secret_body: + if not input_secret_body.get("name"): + raise ValueError( + "name field is mandatory for creating input ObjectStore Secret" + ) + + self.create_or_update_object_store_secret( + context=self, + secret_body=input_secret_body, + is_default=False, + result_key=INPUT_SECRET_SETUP_KEY, + attr_name="input_object_store_secret_name", + creator_mapping=secret_creator_mapping, + replace_existing=replace_existing, + result=result, + ) + + if default_secret_body: + self.create_or_update_object_store_secret( + context=self, + secret_body=default_secret_body, + is_default=True, + result_key=DEFAULT_SECRET_SETUP_KEY, + attr_name="default_object_store_secret_name", + creator_mapping=secret_creator_mapping, + replace_existing=replace_existing, + result=result, + ) + + if self.user_provided_orchestration_url: + result[ORCHESTRATION_URL_SETUP_KEY] = self.orchestration_url + return result + + orchestration_url = self.resolve_orchestration_deployment_url() + self.orchestration_url = orchestration_url + result[ORCHESTRATION_URL_SETUP_KEY] = orchestration_url + + logger.info("One time Setup complete with the created details %s!", result) + return result + + def evaluate( + self, evaluation_configs: List[EvaluationConfig] + ) -> List[EvaluationRun]: + """ + Main evaluate function to create the Evaluation job + + Parameters: + evaluation_configs(List[EvaluationConfig]): A list of one or more of the EvaluationConfig objects + Returns: + List[EvaluationRun]: A list of EvaluationRun objects, one for each EvaluationConfig provided. + + """ + error_collector = ValidationCollector() + # If is instance of one instance convert it to List + if isinstance(evaluation_configs, EvaluationConfig): + evaluation_configs = [evaluation_configs] + try: + # PRE STEP checking if setup is needed and missed + if self.default_object_store_secret_name is None: + # see if default secret exists in the rg + response = fetch_object_store_secret_by_name( + self.ai_core_client, + DEFAULT_KEY, + self.resource_group, + error_collector, + ) + + if response is None: + error_collector.add_error( + ErrorCode.MISSING_DEFAULT_OBJECT_STORE_SECRET_ERROR.value, + "Default Object Store secret is required to run evaluate function. Please use setup() function to create one!", + ) + self.default_object_store_secret_name = DEFAULT_KEY + + if self.orchestration_url is None: + error_collector.add_error( + ErrorCode.MISSING_ORCHESTRATION_URL_ERROR.value, + "Orchestration deployment url is required to run evaluate function. Please use setup() function to create one or pass the value during initialization if already created", + ) + + # raises errors if any collected and missed for setup stage + error_collector.raise_if_errors() + + # STEP0: getting data from files or config provided from the user. + # For now the data object with only AWS creds-> need to extend further for other hyperscalers + # can add a mapper her based on the provider type to incorporate other hyperscalers. + object_store_credentials = {} + if self.provider_name == AWS_PROVIDER_KEY: + object_store_credentials = _AWSObjectStoreData( + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + ) + + # Pre-validation to validate if user provided both orchestration registry and (llm, template) combination + validate_orchestration_params_from_evaluation_config( + evaluation_configs, error_collector + ) + + error_collector.raise_if_errors() + + evaluation_configs_data: List[_EvaluationConfigData] = extract_config_data( + evaluation_configs, + self.ai_core_client, + object_store_credentials, + self.resource_group, + self.__gen_ai_hub_proxy_client, + error_collector, + ) + # to catch any errors while trying to read the files and report + error_collector.raise_if_errors() + + accumulated_config_data, single_execution_flow, reusable_artifact = ( + build_accumulated_config( + evaluation_configs_data, + _has_mixed_config_types(evaluation_configs) + ) + ) + + # validates the orchestration url for each run data along with custom metric config data + if self.user_provided_orchestration_url: + logger.info( + "Validating the orchestration Deployment URL, as it was explicitly passed during initialization and not set up automatically." + ) + # only validate when the user provided the url + validate_orchestration_url_across_configs( + accumulated_config_data, + self.orchestration_url, + self.ai_core_client, + self.resource_group, + error_collector, + self.__gen_ai_hub_proxy_client, + ) + error_collector.raise_if_errors() + + validate_config_data_collection( + accumulated_config_data, + error_collector, + ) + # raise any current errors and stop evaluate if any issues found in validation of the config provided + error_collector.raise_if_errors() + + # Single Execution flow + if single_execution_flow: + evaluation_runs_list = single_evaluation_job_flow( + evaluation_configs, + accumulated_config_data, + self.input_object_store_secret_name, + object_store_credentials, + self.ai_core_client, + self.resource_group, + self.orchestration_url, + error_collector, + ) + return evaluation_runs_list + + # Multiple Executions flow + evaluation_runs_list = multiple_evaluation_jobs_flow( + evaluation_configs, + accumulated_config_data, + self.input_object_store_secret_name, + object_store_credentials, + self.ai_core_client, + self.resource_group, + self.orchestration_url, + reusable_artifact, + error_collector, + ) + return evaluation_runs_list + except Exception as exc: + # If validation errors were collected, raise them with details + # Otherwise, re-raise the original exception + error_collector.raise_if_errors() + raise RuntimeError("Evaluate function failed!") from exc + + # UTIL functions in client + def list_available_models(self): + """ + Method to list all the available llm models + """ + models_info: List[Model] = list_available_llm_models( + self.ai_core_client, self.resource_group + ) + parsed_models_info = [] + for current_model in models_info: + model_extracted_details = current_model.__dict__ + allowed_scenarios_list = model_extracted_details["allowed_scenarios"] + if any( + scenario.get("scenario_id") == "orchestration" + for scenario in allowed_scenarios_list + ): + version_details = [item.__dict__ for item in current_model.versions] + filtered_details = { + k: model_extracted_details[k] for k in ["model", "provider"] + } + filtered_details["versions"] = version_details + parsed_models_info.append(filtered_details) + return parsed_models_info + + def get_system_supported_metrics(self) -> List[str]: + """helper method to get the list of all supported metric ids""" + error_collector = ValidationCollector() + predefined_metric_templates = fetch_all_system_predefined_metrics( + self.ai_core_client, self.resource_group, error_collector + ) + return predefined_metric_templates + +__all__ = ["EvaluationClient", "_has_mixed_config_types"] diff --git a/packages/gen/gen_ai_hub/evaluations/constants.py b/packages/gen/gen_ai_hub/evaluations/constants.py new file mode 100644 index 0000000..b8db63a --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/constants.py @@ -0,0 +1,247 @@ +import os + +DEFAULT_ORCHESTRATION_CONFIG_NAME = "defaultOrchestrationConfig" +EVAL_ORCHESTRATION_CONFIG_PREFIX_NAME = "evalOrchestrationConfig-" +ORCHESTRATION_GLOBAL_SCENARIO_NAME = "orchestration" +DEFAULT_KEY = "default" +DEPLOYMENT_URL_KEY = "deploymentUrl" +JSONL_FILE_TYPE = "jsonl" +CSV_FILE_TYPE = "csv" +JSON_FILE_TYPE = "json" +SUPPORTED_FILE_TYPES = [JSON_FILE_TYPE, JSONL_FILE_TYPE, CSV_FILE_TYPE] +SUFFIX_TO_FILE_TYPE = { + ".json": JSON_FILE_TYPE, + ".jsonl": JSONL_FILE_TYPE, + ".csv": CSV_FILE_TYPE, +} +PROMPT_TEMPLATE_SCENARIO_KEY = "scenario" +PROMPT_TEMPLATE_NAME_KEY = "name" +PROMPT_TEMPLATE_VERSION_KEY = "version" + +PROMPT_TEMPLATE_METADATA_FIELDS = [ + PROMPT_TEMPLATE_SCENARIO_KEY, + PROMPT_TEMPLATE_NAME_KEY, + PROMPT_TEMPLATE_VERSION_KEY, +] +PROMPT_TEMPLATE_ID_KEY = "id" +TEST_PROMPT_TEMPLATE_NAME = "evalPromptTemplateConfig-" +TEST_PROMPT_TEMPLATE_VERSION = "1.0.0" +AI_PROTOCOL_PREFIX = "ai://" +AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" +AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" +PROVIDER_NAME = "PROVIDER_NAME" +AWS_PROVIDER_KEY = "aws" +AWS_OSS_BUCKET_URL_KEY = "storage.ai.sap.com/bucket" +AWS_OSS_REGION_URL_KEY = "storage.ai.sap.com/region" +AWS_OSS_PATH_PREFIX_URL_KEY = "storage.ai.sap.com/pathPrefix" +VARIABLE_MAPPING_PROMPT_PREFIX_KEY = "prompt/" +VARIABLE_MAPPING_DATA_PREFIX_KEY = "data/" +MODULE_CONFIGURATIONS_KEY = "module_configurations" +ORCHESTRATION_CONFIG_KEY = "orchestration_config" +LLM_MODULE_CONFIG_KEY = "llm_module_config" +TEMPLATING_MODULE_CONFIG_KEY = "templating_module_config" +TEMPLATE_KEY = "template" +TEMPLATE_REF_KEY = "template_ref" +MODEL_NAME_KEY = "model_name" +MODEL_VERSION_KEY = "model_version" +LATEST_MODEL_VERSION_KEY = "latest" +MODEL_CONFIGURATION_KEY = "model_configuration" +MODEL_FILTER_LIST_KEY = "modelFilterList" +MODEL_FILTER_LIST_TYPE_KEY = "modelFilterListType" +TEST_TEMPLATE_STRING = "What is Generative AI?" + +ORCHESTRATION_CONFIG_TEMPLATE_V2 = { + "modules": { + "prompt_templating": { + "model": {}, + "prompt": {}, + } + } +} + +ORCHESTRATION_CONFIGURATION_V2 = { + "config": { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "Explain what is Generative AI in 50 words?", + } + ], + "defaults": {}, + }, + "model": {"name": "", "version": "", "params": {}}, + } + } + } +} + +BLEU_METRIC_ID = "bleu" +BERTSCORE_METRIC_ID = "bert_score" +ROUGE_METRIC_ID = "rouge" +JSON_SCHEMA_MATCH_METRIC_ID = "json_schema_match" +JSON_SCHEMA_KEY = "json_schema" +REFERENCE_KEY = "reference" +LANGUAGE_KEY = "language" +CONTENT_FILTER_ON_INPUT_METRIC_ID = "content_filter_on_input" +CONTENT_FILTER_ON_OUTPUT_METRIC_ID = "content_filter_on_output" +EXACT_MATCH_METRIC_ID = "exact_match" +LANGUAGE_MATCH_METRIC_ID = "f3ad2f40-8fcd-41ba-8a9a-fb82469bf99b" +POINTWISE_INSTRUCTION_FOLLOWING_METRIC_ID = "pointwise_instruction_following" +POINTWISE_CORRECTNESS_METRIC_ID = "pointwise_correctness" +POINTWISE_ANSWER_RELEVANCE_METRIC_ID = "pointwise_answer_relevance" +POINTWISE_CONCISENESS_METRIC_ID = "pointwise_conciseness" +POINTWISE_RAG_GROUNDEDNESS_METRIC_ID = "pointwise_rag_groundedness" +POINTWISE_RAG_CONTEXT_RELEVANCE_METRIC_ID = "pointwise_rag_context_relevance" +POINTWISE_RAG_CONTEXT_PRECISION_METRIC_ID = "pointwise_rag_context_precision" +POINTWISE_RAG_COMPLETENESS_METRIC_ID = "pointwise_rag_completeness" +REFERENCE_KEY = "reference" +METRIC_TO_DEPENDENT_VARIABLES_DICT = { + BERTSCORE_METRIC_ID: [REFERENCE_KEY], + BLEU_METRIC_ID: [REFERENCE_KEY], + ROUGE_METRIC_ID: [REFERENCE_KEY], + JSON_SCHEMA_MATCH_METRIC_ID: ["json_schema"], + CONTENT_FILTER_ON_INPUT_METRIC_ID: [], + CONTENT_FILTER_ON_OUTPUT_METRIC_ID: [], + EXACT_MATCH_METRIC_ID: [REFERENCE_KEY], + LANGUAGE_MATCH_METRIC_ID: ["language"], + POINTWISE_INSTRUCTION_FOLLOWING_METRIC_ID: [], + POINTWISE_CORRECTNESS_METRIC_ID: [], + POINTWISE_ANSWER_RELEVANCE_METRIC_ID: [], + POINTWISE_CONCISENESS_METRIC_ID: [], + POINTWISE_RAG_GROUNDEDNESS_METRIC_ID: [], + POINTWISE_RAG_CONTEXT_RELEVANCE_METRIC_ID: [], + POINTWISE_RAG_CONTEXT_PRECISION_METRIC_ID: [], + POINTWISE_RAG_COMPLETENESS_METRIC_ID: [], +} + +SYSTEM_SUPPORTED_METRIC_IDS = [ + BERTSCORE_METRIC_ID, + BLEU_METRIC_ID, + ROUGE_METRIC_ID, + JSON_SCHEMA_MATCH_METRIC_ID, + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, + EXACT_MATCH_METRIC_ID, + LANGUAGE_MATCH_METRIC_ID, + POINTWISE_INSTRUCTION_FOLLOWING_METRIC_ID, + POINTWISE_CORRECTNESS_METRIC_ID, + POINTWISE_ANSWER_RELEVANCE_METRIC_ID, + POINTWISE_CONCISENESS_METRIC_ID, + POINTWISE_RAG_GROUNDEDNESS_METRIC_ID, + POINTWISE_RAG_CONTEXT_RELEVANCE_METRIC_ID, + POINTWISE_RAG_CONTEXT_PRECISION_METRIC_ID, + POINTWISE_RAG_COMPLETENESS_METRIC_ID, +] + +SYSTEM_DEFINED_METRIC_MAPPING = { + BERTSCORE_METRIC_ID: "BERT Score", + BLEU_METRIC_ID: "BLEU", + ROUGE_METRIC_ID: "ROUGE", + JSON_SCHEMA_MATCH_METRIC_ID: "JSON Schema Match", + CONTENT_FILTER_ON_INPUT_METRIC_ID: "Content Filter on Input", + CONTENT_FILTER_ON_OUTPUT_METRIC_ID: "Content Filter on Output", + EXACT_MATCH_METRIC_ID: "Exact Match", + LANGUAGE_MATCH_METRIC_ID: "Language Match", + POINTWISE_INSTRUCTION_FOLLOWING_METRIC_ID: "Pointwise Instruction Following", + POINTWISE_CORRECTNESS_METRIC_ID: "Pointwise Correctness", + POINTWISE_ANSWER_RELEVANCE_METRIC_ID: "Pointwise Answer Relevance", + POINTWISE_CONCISENESS_METRIC_ID: "Pointwise Conciseness", + POINTWISE_RAG_GROUNDEDNESS_METRIC_ID: "Pointwise RAG Groundedness", + POINTWISE_RAG_CONTEXT_RELEVANCE_METRIC_ID: "Pointwise RAG Context Relevance", + POINTWISE_RAG_CONTEXT_PRECISION_METRIC_ID: "Pointwise RAG Context Precision", + POINTWISE_RAG_COMPLETENESS_METRIC_ID: "Pointwise RAG Completeness", +} + +SYSTEM_SUPPORTED_LLM_JUDGE_METRIC_IDS = [ + POINTWISE_INSTRUCTION_FOLLOWING_METRIC_ID, + POINTWISE_CORRECTNESS_METRIC_ID, + POINTWISE_ANSWER_RELEVANCE_METRIC_ID, + POINTWISE_CONCISENESS_METRIC_ID, + POINTWISE_RAG_GROUNDEDNESS_METRIC_ID, + POINTWISE_RAG_CONTEXT_RELEVANCE_METRIC_ID, + POINTWISE_RAG_CONTEXT_PRECISION_METRIC_ID, + POINTWISE_RAG_COMPLETENESS_METRIC_ID, +] + +IMAGE_URL_KEY = "image_url" +CONTENT_KEY = "content" +ROLE_KEY = "role" +TYPE_KEY = "type" +USER_KEY = "user" +FILTERS_KEY = "filters" +GROUNDING_MODULE_CONFIG_KEY = "grounding_module_config" +FILTERING_MODULE_CONFIG_KEY = "filtering_module_config" +AZURE_CONTENT_SAFETY_KEY = "azure_content_safety" +LLAMA_GUARD_CONTENT_SAFETY_KEY = "llama_guard_3_8b" +SUPPORTED_CUSTOM_JUDGE_METRIC_TYPES = ["free-form", "structured", "extension"] +INPUT_VARIABLE_REGEX_PATTERN = r"(?<=\{\{).+?(?=\}\})" +VALIDATION_REGEX_PATTERN_FOR_INPUT_VARIABLES = ( + r"(?!_)(?!.*__)(?!.*--)^[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9]$" +) + +AICORE_LLM_COMPLETION_KEY = "aicore_llm_completion" +AICORE_LLM_PROMPT_TEMPLATE_KEY = "prompt" +AICORE_LLM_GROUNDING_QUERY_KEY = "grounding_query" +AICORE_LLM_GROUNDING_RESPONSE_KEY = "grounding_response" +PREDEFINED_SYSTEM_VARIABLES_LIST = [ + AICORE_LLM_COMPLETION_KEY, + AICORE_LLM_PROMPT_TEMPLATE_KEY, + AICORE_LLM_GROUNDING_QUERY_KEY, + AICORE_LLM_GROUNDING_RESPONSE_KEY, +] + +PROMPT_KEY = "prompt" +ALL_METRICS_COLUMN_MAPPING_KEY = "all_metrics" +COLUMN_MAPPING_DEFAULT_KEYS = [PROMPT_KEY, ALL_METRICS_COLUMN_MAPPING_KEY] + +AI_CORE_PREFIX = "AICORE" +AUTH_ENDPOINT_SUFFIX = "/oauth/token" +CONFIG_FILE_ENV_VAR = f"{AI_CORE_PREFIX}_CONFIG" +DEBUG_ENV_VAR_NAME = "DEBUG" +DEFAULT_HOME_PATH = os.path.join(os.path.expanduser("~"), ".aicore") +HOME_PATH_ENV_VAR = f"{AI_CORE_PREFIX}_HOME" +PROFILE_ENV_VAR = f"{AI_CORE_PREFIX}_PROFILE" +VCAP_AICORE_SERVICE_NAME = "aicore" +VCAP_SERVICES_ENV_VAR = "VCAP_SERVICES" +INPUT_SECRET_SETUP_KEY = "input_secret" +DEFAULT_SECRET_SETUP_KEY = "default_secret" +ORCHESTRATION_URL_SETUP_KEY = "orchestration_url" +AWS_S3_OSS_TYPE_KEY = "S3" +SUPPORTED_OSS_TYPES = [AWS_S3_OSS_TYPE_KEY] +EVALUATIONS_SCENARIO_ID = "genai-evaluations" +EVALUATIONS_CONFIG_PREFIX_KEY = "evaluation-config-" +EVALUATIONS_ARTIFACT_PREFIX_KEY = "evaluation-artifact-" +EVALUATIONS_ARTIFACT_DESCRIPTION = "Artifact for Evaluations Service" +RUNS_FOLDER_KEY = "runs" +DATASET_FOLDER_KEY = "testdata" +RESULTS_FILE_KEY = "results.db" +COMPLETIONS_TABLE_KEY = "submission_result" +METRICS_TABLE_KEY = "evaluation_result" +AGGREGATIONS_TABLE_KEY = "aggregation_result" +OBJECT_STORE_SECRET_EXISTS_MESSAGE = "Secret exists" +ERROR_KEY = "error" +ORCHESTRATION_REGISTRY_ENDPOINT = "/registry/v2/orchestrationConfigs" +AICORE_EXTRA_SUFFIX = "/lm" +METRIC_SERVER_ENDPOINT = "/lm/evaluationMetrics" +EVALUATION_METRICS_ENDPOINT = "/evaluationMetrics" +MODULES_KEY = "modules" +PROMPT_TEMPLATING_KEY = "prompt_templating" +MODEL_KEY = "model" +CONFIG_KEY = "config" +COMPLETION_ENDPOINT_V2 = "/v2/completion" +LLM_MODULE_V2_NAME_KEY = "name" +LLM_MODULE_V2_VERSION_KEY = "version" +LLM_MODULE_V2_PARAMETERS_KEY = "parameters" +PROMPT_REGISTRY_ROLE_KEY = "role" +PROMPT_REGISTRY_CONTENT_KEY = "content" +LLM_AS_A_JUDGE = "llm-as-a-judge" +ID = "id" +VARIABLES_KEY = "variables" +NAME_KEY = "name" +TRACKING_SERVICE_ENDPOINT = "/lm/metrics" +CONTENT_TYPE = "application/json" +DEFAULT_TIMEOUT = 3600 +ADDITIONAL_INFO_KEY = "additional_info" \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/credentials.py b/packages/gen/gen_ai_hub/evaluations/credentials.py new file mode 100644 index 0000000..074afda --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/credentials.py @@ -0,0 +1,290 @@ +from __future__ import annotations + +from typing import Any, Dict, Final, List, Optional, Callable, Tuple +import json +import os +import pathlib + +from dataclasses import dataclass + +from gen_ai_hub.evaluations.constants import ( + AI_CORE_PREFIX, + AUTH_ENDPOINT_SUFFIX, + CONFIG_FILE_ENV_VAR, + PROFILE_ENV_VAR, + VCAP_AICORE_SERVICE_NAME, + VCAP_SERVICES_ENV_VAR, + HOME_PATH_ENV_VAR, + DEFAULT_HOME_PATH, +) +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def get_home() -> str: + return os.environ.get(HOME_PATH_ENV_VAR, DEFAULT_HOME_PATH) + + +def get_nested_value(data_dict, keys: List[str]): + """ + Retrieve a nested value from a dictionary using a list of strings. + + :param data_dict: The dictionary to search. + :param keys: A list of strings representing nested keys. + :return: The value associated with the nested keys, or None if not found. + """ + current_value = data_dict + for key in keys: + current_value = current_value[key] + return current_value + + +@dataclass +class VCAPEnvironment: + services: List[Service] + + @classmethod + def from_env(cls, env_var: Optional[str] = None): + env_var = env_var or VCAP_SERVICES_ENV_VAR + env = json.loads(os.environ.get(env_var, '{}')) + return cls.from_dict(env) + + @classmethod + def from_dict(cls, env: Dict[str, Any]): + services = [Service(service) for services in env.values() for service in services] + return cls(services=services) + + def __getitem__(self, name) -> Service: + return self.get_service(name, exactly_one=True) + + def get_service(self, label, exactly_one: bool = True) -> Service: + services = [s for s in self.services if s.label == label] + if exactly_one: + if len(services) == 0: + raise KeyError(f"No service found with label '{label}'.") + return services[0] + else: + return services + + def get_service_by_name(self, name, exactly_one: bool = True) -> Service: + services = [s for s in self.services if s.name == name] + if exactly_one: + if len(services) == 0: + raise KeyError(f"No service found with name '{name}'.") + return services[0] + else: + return services + + +NoDefault = object() + + +class Service: + + def __init__(self, env: Dict[str, Any]): + self._env = env + + @property + def label(self) -> Optional[str]: + return self._env.get('label') + + @property + def name(self) -> Optional[str]: + return self._env.get('name') + + def __getitem__(self, key): + return self.get(key) + + def get(self, key, default=NoDefault): + if isinstance(key, str): + key_splitted = key.split('.') + else: + key_splitted = key + try: + return get_nested_value(self._env, key_splitted) or default + except KeyError: + if default is NoDefault: + raise KeyError(f"Key '{key}' not found in service '{self.name}'.") + return default + + +@dataclass +class CredentialsValue: + name: str + vcap_key: Optional[Tuple[str, ...]] = None + transform_fn: Optional[Callable] = None + + +@dataclass +class Source: + name: str + get: Callable[[CredentialsValue], Optional[str]] + + +CREDENTIAL_VALUES: Final[List[CredentialsValue]] = [ + CredentialsValue(name='client_id', vcap_key=('credentials', 'clientid')), + CredentialsValue(name='client_secret', vcap_key=('credentials', 'clientsecret')), + CredentialsValue(name='auth_url', + vcap_key=('credentials', 'url'), + transform_fn=lambda url: url.rstrip('/') + + ('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)), + CredentialsValue(name='base_url', + vcap_key=('credentials', 'serviceurls', 'AI_API_URL'), + transform_fn=lambda url: url.rstrip('/') + ('' if url.endswith('/v2') else '/v2')), + CredentialsValue(name='resource_group'), + CredentialsValue(name='cert_url', vcap_key=('credentials', 'certurl'), + transform_fn=lambda url: url.rstrip('/') + + ('' if url.endswith(AUTH_ENDPOINT_SUFFIX) else AUTH_ENDPOINT_SUFFIX)), + # Even though the certificate and key in VCAP_SERVICES are not file paths, the names are defined this way in order + # to keep it compatible with the config names. It'll be handled in fetch_credentials function. + CredentialsValue(name='cert_file_path'), + CredentialsValue(name='key_file_path'), + CredentialsValue(name='cert_str', vcap_key=('credentials', 'certificate'), + transform_fn=lambda cert_str: cert_str.replace('\\n', '\n')), + CredentialsValue(name='key_str', vcap_key=('credentials', 'key'), + transform_fn=lambda key_str: key_str.replace('\\n', '\n')), + # Currently supporting only the AWS creds, would need to extend to other hyperscalers in future. + CredentialsValue(name='aws_access_key_id'), + CredentialsValue(name='aws_secret_access_key'), + CredentialsValue(name='orchestration_url'), + CredentialsValue(name='input_object_store_secret_name'), +] + + +def init_conf(profile: str = None): + # Read configuration from ${AICORE_HOME}/config_.json. + home = pathlib.Path(get_home()) + profile = profile or os.environ.get(PROFILE_ENV_VAR) + profile_config_file = f'config_{profile}.json' + direct_config_file = pathlib.Path(os.getenv(CONFIG_FILE_ENV_VAR)) if os.getenv(CONFIG_FILE_ENV_VAR) else None + path_to_config = (direct_config_file or + (home / ('config.json' if profile in ('default', '', None) else profile_config_file))) + config = {} + if path_to_config.exists(): + logger.debug('Config file path %s', path_to_config) + try: + with path_to_config.open(encoding='utf-8') as f: + return json.load(f) + except json.decoder.JSONDecodeError: + raise KeyError(f'{path_to_config} is not a valid json file. Please fix or remove it!') + except PermissionError as e: + logger.warning("Permission denied when trying to read config file '%s'. File ignored.", path_to_config) + return config + elif profile: + raise FileNotFoundError(f"Unable to locate profile config file '{profile_config_file}' " + f"in AICORE_HOME '{home}')") + return config + + +def extract_credentials(source: Source, exclude: List[str] = None) -> Dict[str, str]: + """Extract all credentials from a source.""" + exclude = exclude or [] + credentials = {} + for cv in CREDENTIAL_VALUES: + if cv.name in exclude: + continue + if value := source.get(cv): + credentials[cv.name] = cv.transform_fn(value) if cv.transform_fn else value + return credentials + + +def resolve_credentials(sources: List[Source]) -> Dict[str, str]: + """Extract credentials from the first source that has any defined.""" + for source in sources: + if credentials := extract_credentials(source, exclude=['resource_group']): + logger.debug(f"Using credentials from: {source.name}") + return credentials + raise ValueError("No credentials found in any source") + + +def resolve_resource_group(sources: List[Source]) -> Optional[str]: + """Find resource_group from the first source that defines it.""" + rg_cred = CredentialsValue(name='resource_group') + for source in sources: + if value := source.get(rg_cred): + logger.debug("Using resource_group '%s' from: %s", value, source.name) + return value + logger.debug("No resource_group found in any source") + return None + + +def validate_credentials(credentials: Dict[str, str]) -> None: + """Validate that we have a complete authentication method.""" + required_base = {'client_id', 'auth_url', 'base_url'} + + # Check which auth method we have + has_client_secret = 'client_secret' in credentials + has_cert_files = 'cert_file_path' in credentials and 'key_file_path' in credentials + has_cert_strings = 'cert_str' in credentials and 'key_str' in credentials + + # Must have exactly one auth method + auth_methods = sum([has_client_secret, has_cert_files, has_cert_strings]) + + if auth_methods == 0: + raise ValueError( + "No authentication method found. Must provide one of:\n" + "1. client_secret\n" + "2. cert_file_path AND key_file_path\n" + "3. cert_str AND key_str" + ) + + if auth_methods > 1: + raise ValueError( + "Multiple authentication methods found. Please provide only one of:\n" + "1. client_secret\n" + "2. cert_file_path AND key_file_path\n" + "3. cert_str AND key_str" + ) + + # Check required base fields + missing = required_base - set(credentials.keys()) + if missing: + raise ValueError(f"Missing required credentials: {missing}") + + +def _str_or_none(value) -> Optional[str]: + return str(value) if value else None + + +def fetch_credentials(profile: str = None, **kwargs) -> Dict[str, str]: + """ + Fetch credentials from a single source based on precedence. + + Precedence order: kwargs > environment variables > config file > VCAP service + + Once a source is selected (first one with any credential), all credentials + come from that source only. Resource group is an exception and follows + precedence independently. + """ + config = init_conf(profile=profile) + + try: + vcap_service = VCAPEnvironment.from_env()[VCAP_AICORE_SERVICE_NAME] + except KeyError: + vcap_service = None + + sources = [ + Source("kwargs", + lambda cv: _str_or_none(kwargs.get(cv.name))), + Source("environment variables", + lambda cv: _str_or_none(os.environ.get(f'{AI_CORE_PREFIX}_{cv.name.upper()}'))), + Source("config file", + lambda cv: _str_or_none(config.get(f'{AI_CORE_PREFIX}_{cv.name.upper()}'))), + Source("VCAP service", + lambda cv: _str_or_none(vcap_service.get(cv.vcap_key, None) if vcap_service and cv.vcap_key else None)), + ] + + credentials = resolve_credentials(sources) + + # Use cert_url as auth_url if present (VCAP provides cert_url for certificate auth) + if 'cert_url' in credentials: + credentials['auth_url'] = credentials.pop('cert_url') + + validate_credentials(credentials) + + resource_group = resolve_resource_group(sources) + if resource_group: + credentials['resource_group'] = resource_group + + return credentials diff --git a/packages/gen/gen_ai_hub/evaluations/exceptions/__init__.py b/packages/gen/gen_ai_hub/evaluations/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/evaluations/exceptions/error_codes.py b/packages/gen/gen_ai_hub/evaluations/exceptions/error_codes.py new file mode 100644 index 0000000..cc89dec --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/exceptions/error_codes.py @@ -0,0 +1,54 @@ +""" +Module containing various error codes and messages which evaluation sdk uses +""" + +from enum import Enum, unique + + +@unique +class ErrorCode(Enum): + """Class defining own submodule specific error codes""" + + GET_OBJECT_STORE_SECRET_ERROR = "0000" + MISSING_ORCHESTRATION_URL_ERROR = "0001" + INVALID_ORCHESTRATION_CONFIG_ERROR = "0002" + INVALID_FILE_PATH_ERROR = "0003" + UNSUPPORTED_FILE_TYPE_ERROR = "0004" + INVALID_JSON_DECODING_ERROR = "0005" + GENERIC_ERROR = "0006" + INVALID_ARTIFACT_URL_ERROR = "0007" + INVALID_OBJECT_STORE_SECRET_ERROR = "0008" + INVALID_S3_CLIENT_ERROR = "0009" + READ_FILE_DATA_FROM_ARTIFACT_ERROR = "0010" + EMPTY_FILE_DATA_ERROR = "0011" + INVALID_TEMPLATE_REFERENCE_KEY = "0012" + INVALID_DEPLOYMENT_STATUS = "0013" + MODEL_NOT_ALLOWED_ERROR = "0014" + INVALID_ORCHESTRATION_URL_ERROR = "0015" + EMPTY_METRIC_ERROR = "0016" + EMPTY_METRIC_NAME_ERROR = "0017" + UNSUPPORTED_METRIC_ERROR = "0018" + INVALID_CUSTOM_METRIC_ERROR = "0019" + EMPTY_FIELD_NAME_ERROR = "0020" + INVALID_TEMPLATE_MODULE_CONFIG_ERROR = "0021" + FILE_UPLOAD_ERROR = "0022" + ARTIFACT_CREATION_FAILURE = "0023" + CONFIGURATION_CREATION_FAILURE = "0024" + EXECUTION_CREATION_FAILURE = "0025" + INVALID_METRIC_MAPPING_ERROR = "0026" + UNSUPPORTED_LANGUAGE_MATCH_ERROR = "0027" + MISSING_DEFAULT_OBJECT_STORE_SECRET_ERROR = "0028" + PROMPT_TEMPLATE_GET_ERROR = "0029" + REGISTER_PROMPT_TEMPLATE_ERROR = "0030" + METRIC_SERVER_RESOLVE_ERROR = "0031" + INVALID_FILTER_TYPE_ERROR = "0032" + ORCHESTRATION_URL_VALIDATION_ERROR = "0033" + METRIC_CONFIG_ERROR = "0034" + MISSING_USER_PROMPT_ERROR = "0035" + MORE_THAN_ONE_USER_PROMPT_PROVIDED_ERROR = "0036" + EMPTY_TEMPLATE_LIST_ERROR = "0037" + EMPTY_TEMPLATE_LIST_URL_ERROR = "0038" + INVALID_GROUNDING_CONFIGURATION = "0039" + UNSUPPORTED_FILTER_TYPE_ERROR = "0040" + INVALID_DATASET_DATA_ERROR = "0041" + INVALID_PARAMETER_VALUE_ERROR = "0042" diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/__init__.py b/packages/gen/gen_ai_hub/evaluations/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/collector.py b/packages/gen/gen_ai_hub/evaluations/helpers/collector.py new file mode 100644 index 0000000..d7dacee --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/helpers/collector.py @@ -0,0 +1,41 @@ +class ValidationCollector: + """ + A class to collect and manage validation errors encountered during the orchestration configuration validation process. + """ + + def __init__(self): + self.errors = [] + + def add_error(self, internal_code, message): + """ + Adds an error to the collector. + Args: + internal_code (str): The internal error code. + message (str): The detailed error message. + """ + self.errors.append((internal_code, message)) + + def has_errors(self): + """ + checks if the collector collected any errors + """ + return len(self.errors) > 0 + + def has_error_code(self, code): + """checks if error code exists in the list of errors""" + return any(error_code == code for error_code, _ in self.errors) + + def raise_if_errors(self): + """ + Raises a ValidationError if there are any collected errors. + """ + if self.has_errors(): + error_messages = "\n".join( + [ + f"Error Code: {code}, Detailed Message: {msg}" + for code, msg in self.errors + ] + ) + raise RuntimeError( + f"\nConfiguration error(s) encountered:\n{error_messages}" + ) diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/config_data.py b/packages/gen/gen_ai_hub/evaluations/helpers/config_data.py new file mode 100644 index 0000000..3708aa2 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/helpers/config_data.py @@ -0,0 +1,204 @@ +from typing import List, Union, Tuple +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations._internal._models import ( + _EvaluationConfigData, + _AWSObjectStoreData, +) +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.utils.config_data_utils import ( + get_orch_config_data, + get_dataset_data, +) +from gen_ai_hub.evaluations.utils.gen_utils import ( + get_accumulated_config_data, + update_variable_mapping, +) +from gen_ai_hub.evaluations.constants import VARIABLE_MAPPING_PROMPT_PREFIX_KEY +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubProxyClient +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.logging import get_logger +from gen_ai_hub.evaluations.utils.aicore_utils import ( + resolve_metric_identifiers, + resolve_metric_names, +) + +logger = get_logger() + + +def extract_config_data( + evaluation_configs: List[EvaluationConfig], + ai_core_client: AICoreV2Client, + object_store_credentials: _AWSObjectStoreData, + resource_group: str, + gen_ai_hub_proxy_client: GenAIHubProxyClient, + error_collector: ValidationCollector, +) -> List[_EvaluationConfigData]: + """Extract configuration data from user-provided evaluation configs. + + This function processes evaluation configurations to extract orchestration config, + dataset data, metric templates, and variable mappings for each configuration. + + :param evaluation_configs: List of evaluation configuration objects to process + :type evaluation_configs: List[EvaluationConfig] + :param ai_core_client: AI Core V2 client for API interactions + :type ai_core_client: AICoreV2Client + :param object_store_credentials: Credentials for accessing object storage (AWS S3) + :type object_store_credentials: _AWSObjectStoreData + :param resource_group: AI Core resource group name + :type resource_group: str + :param gen_ai_hub_proxy_client: GenAI Hub proxy client for orchestration operations + :type gen_ai_hub_proxy_client: GenAIHubProxyClient + :param error_collector: Collector for validation errors + :type error_collector: ValidationCollector + :return: List of extracted evaluation configuration data objects + :rtype: List[_EvaluationConfigData] + """ + result: List[_EvaluationConfigData] = [] + logger.info("Extracting data from the Configuration provided!") + try: + # builds the variable mapping + for evaluation_config in evaluation_configs: + variable_mapping_dict = {} # individual variable_mapping for each of the config object provided by user + logger.info( + "For the current evaluation config of %s", evaluation_config.__dict__ + ) + orch_config_data = get_orch_config_data( + evaluation_config, + ai_core_client, + gen_ai_hub_proxy_client, + error_collector, + ) + dataset_type = evaluation_config.dataset_config.file_type + dataset_data = get_dataset_data( + evaluation_config.dataset_config, + ai_core_client, + object_store_credentials, + resource_group, + error_collector, + ) + # handling the template variable mapping for prompt and dataset + if evaluation_config.template_variable_mapping is not None: + variable_mapping_dict = update_variable_mapping( + evaluation_config.template_variable_mapping, + VARIABLE_MAPPING_PROMPT_PREFIX_KEY, + variable_mapping_dict, + ) + + # fetch the data and store in resolved_metrics_data + metric_templates_data = resolve_metric_identifiers( + evaluation_config.metrics, + ai_core_client, + resource_group, + error_collector, + ) + + + # stores resolved metric names provided via metricConfig + metrics_list = resolve_metric_names( + evaluation_config.metrics, error_collector + ) + + for index, metric in enumerate(evaluation_config.metrics): + # handling the variable mapping for metrics + if metric.variable_mapping is not None: + prefix_key = metrics_list[index] + "/" + variable_mapping_dict = update_variable_mapping( + metric.variable_mapping, + prefix_key, + variable_mapping_dict, + ) + + current_config_data = _EvaluationConfigData( + orch_config_data=[orch_config_data], + dataset_type=dataset_type, + dataset_data=dataset_data, + metrics_list=metrics_list, + metric_templates=metric_templates_data, + variable_mapping=variable_mapping_dict, + test_row_count=evaluation_config.test_row_count, + tags=evaluation_config.tags, + repetitions=evaluation_config.repetitions, + debug_mode=evaluation_config.debug_mode, + ) + + if not orch_config_data: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + f"Orchestration config data provided from the evaluation configuration is empty. Please provide a valid combination of (llm and template) or orchestration_registry reference for this config: {evaluation_config.__dict__}", + ) + + if not dataset_data: + error_collector.add_error( + ErrorCode.EMPTY_FILE_DATA_ERROR, + f"Dataset config file data is empty, please provide a valid path or artifact for this datasetConfig: {evaluation_config.dataset_config}", + ) + + # all configs from user mapping to jsonl type as there can be multiple metrics and reading data already in the array + logger.info( + "Extracted data for the current evaluation config is %s", + current_config_data, + ) + result.append(current_config_data) + + return result + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Data extraction of the evaluation config provided failed with error of {e}", + ) + return result # empty result which just obeys the config data dict + + +def build_accumulated_config( + evaluation_configs_data: List[_EvaluationConfigData], + has_mixed_config_types: bool = False, +) -> Tuple[Union[List[_EvaluationConfigData], _EvaluationConfigData], bool, bool]: + """Build accumulated configuration data and determine execution flow strategy. + + Analyzes evaluation configurations to determine whether they can be executed + as a single job (if datasets and metrics match) or require multiple executions. + Also determines if artifacts can be reused across executions. + + :param evaluation_configs_data: List of extracted evaluation configuration data objects + :type evaluation_configs_data: List[_EvaluationConfigData] + :param has_mixed_config_types: Whether evaluation configs have mixed types (llm+template and orchestration_registry) + :type has_mixed_config_types: bool + :return: Tuple containing: + - accumulated_config_data: Either a single accumulated config or list of configs + - single_execution_flow: True if all configs can be executed as one job + - reusable_artifact: True if dataset artifact can be reused across executions + :rtype: Tuple[Union[List[_EvaluationConfigData], _EvaluationConfigData], bool, bool] + """ + single_execution_flow = False + reusable_artifact = False + accumulated_config_data: List[_EvaluationConfigData] | _EvaluationConfigData = None + fetched_dataset_data = evaluation_configs_data[0].dataset_data + fetched_metrics_list = evaluation_configs_data[0].metrics_list + all_datasets_data_same = all( + current_eval_config_data.dataset_data == fetched_dataset_data + for current_eval_config_data in evaluation_configs_data + ) + all_metrics_list_same = all( + set(current_eval_config_data.metrics_list) == set(fetched_metrics_list) + for current_eval_config_data in evaluation_configs_data + ) + + # A single execution cannot handle both llm+template and orchestration_registry types together + if all_datasets_data_same and all_metrics_list_same and not has_mixed_config_types: + # to create one artifact and one execution + # As decided now all config will be mapped to one single execution so one single config + accumulated_config_data = get_accumulated_config_data(evaluation_configs_data) + single_execution_flow = ( + True # as we are accumulating the data, only one single execution + ) + elif all_datasets_data_same: + # to create one artifact in case of multiple executions + reusable_artifact = True + + if not accumulated_config_data: + # Not a single execution flow so re-create multiple executions + accumulated_config_data = evaluation_configs_data + + + return (accumulated_config_data, single_execution_flow, reusable_artifact) diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/evaluation_optimization_flow.py b/packages/gen/gen_ai_hub/evaluations/helpers/evaluation_optimization_flow.py new file mode 100755 index 0000000..bd6b68f --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/helpers/evaluation_optimization_flow.py @@ -0,0 +1,372 @@ +import uuid +from typing import List +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_api_client_sdk.models.artifact import Artifact +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations._internal._models import ( + _EvaluationConfigData, + _AWSObjectStoreData, +) +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.utils.aicore_utils import ( + upload_evaluation_dataset_data, + register_aicore_artifact, + register_aicore_configuration, + register_aicore_execution, +) +from gen_ai_hub.evaluations.constants import DEFAULT_KEY +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.models.evaluation_run import EvaluationRun +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def upload_dataset_data_and_register_aicore_artifact( + input_object_store_secret_name: str, + accumulated_config_data: _EvaluationConfigData, + object_store_credentials: _AWSObjectStoreData, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + current_object_store_secret_name = ( + input_object_store_secret_name + if input_object_store_secret_name + else DEFAULT_KEY + ) + ( + artifact_folder_path, + aicore_configuration_dataset_path, + ) = upload_evaluation_dataset_data( + accumulated_config_data, + object_store_credentials, + current_object_store_secret_name, + ai_core_client, + resource_group, + error_collector, + ) + if artifact_folder_path == "": + error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, + "Error while uploading the files to the provided object store secret, so terminating the evaluate function. Please look into the error and try again", + ) + + logger.info( + "After uploading the data to the hyperscaler, the root folder path is %s. The dataset path is %s", + artifact_folder_path, + aicore_configuration_dataset_path, + ) + + error_collector.raise_if_errors() + # STEP3: artifact creation + aicore_artifact_id = register_aicore_artifact( + artifact_folder_path, + ai_core_client, + resource_group, + current_object_store_secret_name, + error_collector, + ) + + if aicore_artifact_id == "": + error_collector.add_error( + ErrorCode.ARTIFACT_CREATION_FAILURE, + "Error while registering the artifact with the provided config files, so terminating the evaluate function. Please look into the error and try again", + ) + logger.info("AI Core artifact ID created: %s", aicore_artifact_id) + error_collector.raise_if_errors() + + return (aicore_artifact_id, aicore_configuration_dataset_path) + + +def configure_dataset_artifact( + evaluation_configs: List[EvaluationConfig], + accumulated_config_data: _EvaluationConfigData, # single execution so config is accumulated + input_object_store_secret_name: str, + object_store_credentials: _AWSObjectStoreData, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + dataset_config_instance = evaluation_configs[0].dataset_config + dataset_folder_artifact_path = "" + aicore_artifact_id = "" + + if isinstance(dataset_config_instance.source, ArtifactSource): + # Single artifact and it's the same artifact instance across the entire config list. + artifact_instance = dataset_config_instance.source.artifact + if isinstance(artifact_instance, Artifact): + dataset_folder_artifact_path = artifact_instance.id + else: + dataset_folder_artifact_path = artifact_instance # direct string instance. + + if dataset_folder_artifact_path != "": + aicore_artifact_id = dataset_folder_artifact_path + aicore_configuration_dataset_path = dataset_config_instance.source.path + logger.info( + "Dataset artifact fetched: %s, dataset config path: %s", + aicore_artifact_id, + aicore_configuration_dataset_path, + ) + logger.info( + "AI Core artifact ID being reused for AI Core configuration: %s", + aicore_artifact_id, + ) + + else: + (aicore_artifact_id, aicore_configuration_dataset_path) = ( + upload_dataset_data_and_register_aicore_artifact( + input_object_store_secret_name, + accumulated_config_data, + object_store_credentials, + ai_core_client, + resource_group, + error_collector, + ) + ) + + return (aicore_artifact_id, aicore_configuration_dataset_path) + + +def configure_orchestration_config_for_simplified_executable( + evaluation_configs: List[EvaluationConfig], +): + # Logic to generate the uuids required for llm,prompt combination or orchestration_registry list of uuids + llm_model_config = None + template_config = None + orchestration_registry_config = None + template_config_list = [] + model_config_list = [] + orchestration_registry_config_list = [] + current_config = evaluation_configs[0] + if ( + current_config.llm is not None + ): # JUST TO check whether user provided llm,template combination or orchestration_regostry_combination + # user provided llm and template combination + for config in evaluation_configs: + llm_model_name = config.llm.name + llm_model_version = config.llm.version + model_config = f"{llm_model_name}:{llm_model_version}" + model_config_list.append(model_config) + template_config_list.append(config.template) + # building the actual config object needed for underlying executable + llm_model_config = ",".join(model_config_list) + template_config = template_config_list + else: + # user provided orchestration registry uuids + for config in evaluation_configs: + orchestration_registry_config_list.append( + config.orchestration_registry_reference + ) + # building the actual config object needed for underlying executable + orchestration_registry_config = ",".join(orchestration_registry_config_list) + + return llm_model_config, template_config, orchestration_registry_config + + +def single_evaluation_job_flow( + evaluation_configs: List[EvaluationConfig], + accumulated_config_data: _EvaluationConfigData, # single execution so config is accumulated + input_object_store_secret_name: str, + object_store_credentials: _AWSObjectStoreData, + ai_core_client: AICoreV2Client, + resource_group: str, + orchestration_url: str, + error_collector: ValidationCollector, +): + aicore_artifact_id, aicore_configuration_dataset_path = configure_dataset_artifact( + evaluation_configs, + accumulated_config_data, + input_object_store_secret_name, + object_store_credentials, + ai_core_client, + resource_group, + error_collector, + ) + # STEP4: config creation + # generates the list of run_ids to be attached to the execution executable based on length of orchestration_configs provoded by user + run_ids_list = [ + uuid.uuid4().hex for _ in range(len(accumulated_config_data.orch_config_data)) + ] + + llm_model_config, template_config, orchestration_registry_config = ( + configure_orchestration_config_for_simplified_executable(evaluation_configs) + ) + + aicore_configuration_id = register_aicore_configuration( + aicore_artifact_id, + ai_core_client, + resource_group, + accumulated_config_data, + orchestration_url, + aicore_configuration_dataset_path, + run_ids_list, + llm_model_config, + template_config, + orchestration_registry_config, + error_collector, + ) + logger.info("AI Core configuration ID: %s", aicore_configuration_id) + + if aicore_configuration_id == "": + error_collector.add_error( + ErrorCode.CONFIGURATION_CREATION_FAILURE, + "Error while creating the aicore configuration with the provided config files, so terminating the evaluate function. Please look into the error and try again", + ) + + error_collector.raise_if_errors() + + # STEP5: execution creation + + aicore_execution_id = register_aicore_execution( + ai_core_client, + aicore_configuration_id, + resource_group, + error_collector, + ) + + logger.info("AI Core execution ID: %s", aicore_execution_id) + + if aicore_execution_id == "": + error_collector.add_error( + ErrorCode.EXECUTION_CREATION_FAILURE, + "Error while creating the aicore execution with the provided config files, so terminating the evaluate function. Please look into the error and try again", + ) + + evaluation_runs: List[EvaluationRun] = [] + for run_id in run_ids_list: + run = EvaluationRun( + run_id=run_id, + execution_id=aicore_execution_id, + configuration_id=aicore_configuration_id, + artifact_id=aicore_artifact_id, + ai_core_client=ai_core_client, + resource_group=resource_group, + object_store_credentials=object_store_credentials, # credentials object mapped to get the results based on run id. + metrics_list=accumulated_config_data.metrics_list, # passing the specific metric ids list to the run for parsing results. + ) + evaluation_runs.append(run) + return evaluation_runs + + +def multiple_evaluation_jobs_flow( + evaluation_configs: List[EvaluationConfig], + accumulated_config_data: List[_EvaluationConfigData], + input_object_store_secret_name: str, + object_store_credentials: _AWSObjectStoreData, + ai_core_client: AICoreV2Client, + resource_group: str, + orchestration_url: str, + reusable_artifact: bool, + error_collector: ValidationCollector, +): + artifact_ids_list = [] + aicore_configuration_dataset_paths_list = [] + if reusable_artifact: + aicore_artifact_id, aicore_configuration_dataset_path = ( + configure_dataset_artifact( + evaluation_configs, + accumulated_config_data[ + 0 + ], # as data across all datasets are the same, using the first config object + input_object_store_secret_name, + object_store_credentials, + ai_core_client, + resource_group, + error_collector, + ) + ) + artifact_ids_list = [aicore_artifact_id] + aicore_configuration_dataset_paths_list = [aicore_configuration_dataset_path] + else: + # create multiple artifacts for each of config data provided. + for current_accumulated_config in accumulated_config_data: + (aicore_artifact_id, aicore_configuration_dataset_path) = ( + upload_dataset_data_and_register_aicore_artifact( + input_object_store_secret_name, + current_accumulated_config, + object_store_credentials, + ai_core_client, + resource_group, + error_collector, + ) + ) + + artifact_ids_list.append(aicore_artifact_id) + aicore_configuration_dataset_paths_list.append( + aicore_configuration_dataset_path + ) + + evaluation_runs: List[EvaluationRun] = [] + + for index, current_accumulated_config in enumerate(accumulated_config_data): + aicore_artifact_id = ( + artifact_ids_list[index] + if index < len(artifact_ids_list) + else artifact_ids_list[0] + ) + # STEP4: config creation + # generates the list of run_ids to be attached to the execution executable based on length of orchestration_configs provoded by user + run_ids_list = [ + uuid.uuid4().hex + for _ in range(len(current_accumulated_config.orch_config_data)) + ] + + llm_model_config, template_config, orchestration_registry_config = ( + configure_orchestration_config_for_simplified_executable( + [ + evaluation_configs[index] + ] # using that particular configs data of llm and template + ) + ) + aicore_configuration_id = register_aicore_configuration( + aicore_artifact_id, + ai_core_client, + resource_group, + current_accumulated_config, + orchestration_url, + aicore_configuration_dataset_path, + run_ids_list, + llm_model_config, + template_config, + orchestration_registry_config, + error_collector, + ) + if aicore_configuration_id == "": + error_collector.add_error( + ErrorCode.CONFIGURATION_CREATION_FAILURE, + "Error while creating the aicore configuration with the provided config files, so terminating the evaluate function. Please look into the error and try again", + ) + error_collector.raise_if_errors() + logger.info("AI Core configuration ID: %s", aicore_configuration_id) + + # STEP5: execution creation + aicore_execution_id = register_aicore_execution( + ai_core_client, + aicore_configuration_id, + resource_group, + error_collector, + ) + + if aicore_execution_id == "": + error_collector.add_error( + ErrorCode.EXECUTION_CREATION_FAILURE, + "Error while creating the aicore execution with the provided config files, so terminating the evaluate function. Please look into the error and try again", + ) + error_collector.raise_if_errors() + logger.info("AI Core execution ID: %s", aicore_execution_id) + + for run_id in run_ids_list: + evaluation_run = EvaluationRun( + run_id=run_id, + execution_id=aicore_execution_id, + configuration_id=aicore_configuration_id, + artifact_id=aicore_artifact_id, + ai_core_client=ai_core_client, + resource_group=resource_group, + object_store_credentials=object_store_credentials, + metrics_list=current_accumulated_config.metrics_list, + ) + evaluation_runs.append(evaluation_run) + return evaluation_runs diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/logging.py b/packages/gen/gen_ai_hub/evaluations/helpers/logging.py new file mode 100644 index 0000000..cde25b1 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/helpers/logging.py @@ -0,0 +1,23 @@ +import logging +import os +from gen_ai_hub.evaluations.constants import DEBUG_ENV_VAR_NAME + + +BASE_LOGGER_NAME = "gen_ai_evaluations_sdk" +DEFAULT_LOG_LEVEL = logging.INFO + + +def get_logger(name: str = None): + # Use a hierarchical logger structure to allow for more granular control + logger_name = f"{name}" if name else BASE_LOGGER_NAME + return logging.getLogger(logger_name) + + +def set_log_level(logger: logging.Logger, default_level=DEFAULT_LOG_LEVEL): + # Check if DEBUG is set to "true" (case-insensitive) + debug_env = os.getenv(DEBUG_ENV_VAR_NAME) + debug = debug_env is not None and debug_env.lower() == 'true' + logger.setLevel(logging.DEBUG if debug else default_level) + + +set_log_level(get_logger()) diff --git a/packages/gen/gen_ai_hub/evaluations/helpers/s3_file_client.py b/packages/gen/gen_ai_hub/evaluations/helpers/s3_file_client.py new file mode 100644 index 0000000..db286c1 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/helpers/s3_file_client.py @@ -0,0 +1,423 @@ +import boto3 +import os +from typing import Dict, List, Any +import json +import csv +import pandas as pd +import sqlite3 +from io import StringIO +import tempfile +from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.logging import get_logger +from gen_ai_hub.evaluations.constants import CONTENT_TYPE + +logger = get_logger() + + +class S3FileClient: + """S3 client for read/write file operations with format-specific parsing.""" + + def __init__( + self, + bucket_name: str, + region: str = None, + aws_access_key_id: str = None, + aws_secret_access_key: str = None, + error_collector: ValidationCollector = None, + ): + """Initialize S3 client with flexible authentication options. + + :param bucket_name: S3 bucket name + :type bucket_name: str + :param region: AWS region (defaults to boto3 default), defaults to None + :type region: str, optional + :param aws_access_key_id: AWS access key (optional if using IAM/profile), defaults to None + :type aws_access_key_id: str, optional + :param aws_secret_access_key: AWS secret key (optional if using IAM/profile), defaults to None + :type aws_secret_access_key: str, optional + :param error_collector: Validation error collector, defaults to None + :type error_collector: ValidationCollector, optional + """ + self.bucket_name = bucket_name + self.error_collector = error_collector + + try: + session = boto3.Session( + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=region, + ) + + self.s3_client = session.client("s3") + self.region = region or session.region_name + + # Validate bucket access + self._validate_bucket_access() + + except NoCredentialsError: + self.error_collector.add_error( + ErrorCode.INVALID_S3_CLIENT_ERROR, + "AWS credentials not found. Please provide credentials via " + "environment variables", + ) + except Exception as e: + self.error_collector.add_error( + ErrorCode.INVALID_S3_CLIENT_ERROR, + f"Failed to initialize S3 client: {e}", + ) + + def _validate_bucket_access(self): + """Validate that we can access the specified bucket. + + :raises ClientError: If bucket access fails + """ + try: + self.s3_client.head_bucket(Bucket=self.bucket_name) + logger.info(f"Successfully connected to bucket: {self.bucket_name}") + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "404": + self.error_collector.add_error( + ErrorCode.INVALID_S3_CLIENT_ERROR, + f"Bucket '{self.bucket_name}' not found", + ) + elif error_code == "403": + self.error_collector.add_error( + ErrorCode.INVALID_S3_CLIENT_ERROR, + f"Access denied to bucket '{self.bucket_name}'", + ) + else: + self.error_collector.add_error( + ErrorCode.INVALID_S3_CLIENT_ERROR, + f"Error accessing bucket '{self.bucket_name}': {e}", + ) + + def read_json(self, s3_key: str, encoding: str = "utf-8") -> Dict[str, Any]: + """Read JSON file from S3. + + :param s3_key: S3 object key + :type s3_key: str + :param encoding: File encoding, defaults to "utf-8" + :type encoding: str, optional + :return: Dictionary containing JSON data, or empty list if file is empty or error occurs + :rtype: Dict[str, Any] + """ + try: + response = self.s3_client.get_object(Bucket=self.bucket_name, Key=s3_key) + content = response["Body"].read().decode(encoding) + + # Handle empty files + if not content.strip(): + logger.warning(f"Empty JSON file: s3://{self.bucket_name}/{s3_key}") + return [] + + data = json.loads(content) + logger.info(f"Successfully read JSON file from s3://{self.bucket_name}/{s3_key}") + return data + + except json.JSONDecodeError as e: + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + f"Invalid JSON in s3://{self.bucket_name}/{s3_key}: {e}", + ) + return [] + except (BotoCoreError, ClientError) as e: + error_message = ( + f"Failed to read JSON from S3: s3://{self.bucket_name}/{s3_key} - {e}" + ) + + if getattr(e, "response", {}).get("Error", {}).get("Code") == "NoSuchKey": + error_message = f"File not found: s3://{self.bucket_name}/{s3_key}" + + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, error_message + ) + return [] + except Exception as e: # any other generic excedptions + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + f"Unexpected error reading JSON from S3: {e}", + ) + return [] + + def read_jsonl(self, s3_key: str, encoding: str = "utf-8") -> List[Dict[str, Any]]: + """Read JSONL (JSON Lines) file from S3. + + :param s3_key: S3 object key + :type s3_key: str + :param encoding: File encoding, defaults to "utf-8" + :type encoding: str, optional + :return: List of dictionaries, one per line + :rtype: List[Dict[str, Any]] + """ + try: + response = self.s3_client.get_object(Bucket=self.bucket_name, Key=s3_key) + content = response["Body"].read().decode(encoding) + + if not content.strip(): + logger.warning(f"Empty JSONL file: s3://{self.bucket_name}/{s3_key}") + return [] + + data = [] + valid_lines = 0 + invalid_lines = 0 + + for line_num, line in enumerate(content.strip().split("\n"), 1): + line = line.strip() + if not line: + continue + + try: + data.append(json.loads(line)) + valid_lines += 1 + except json.JSONDecodeError as e: + invalid_lines += 1 + logger.warning(f"Skipping invalid JSON on line {line_num}: {e}") + continue + + logger.info( + f"Successfully read JSONL file from s3://{self.bucket_name}/{s3_key} " + f"({valid_lines} valid records, {invalid_lines} skipped)" + ) + return data + + except (BotoCoreError, ClientError) as e: + error_message = ( + f"Failed to read JSONL from S3: s3://{self.bucket_name}/{s3_key} - {e}" + ) + + if getattr(e, "response", {}).get("Error", {}).get("Code") == "NoSuchKey": + error_message = f"File not found: s3://{self.bucket_name}/{s3_key}" + + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, error_message + ) + return [] + + except Exception as e: + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + f"Unexpected error reading JSONL from S3: {e}", + ) + return [] + + def read_csv(self, s3_key: str, encoding: str = "utf-8") -> List[Dict[str, Any]]: + """Read CSV file from S3. + + :param s3_key: S3 object key + :type s3_key: str + :param encoding: File encoding, defaults to "utf-8" + :type encoding: str, optional + :return: List of dictionaries, one per row + :rtype: List[Dict[str, Any]] + """ + try: + response = self.s3_client.get_object(Bucket=self.bucket_name, Key=s3_key) + content = response["Body"].read().decode(encoding) + + df = pd.read_csv( + StringIO(content), + quoting=1, + escapechar="\\", + encoding="utf-8", + keep_default_na=False, + dtype=str, + ) + + logger.info(f"Successfully read CSV file from s3://{self.bucket_name}/{s3_key}") + return df.to_dict("records") + + except csv.Error as e: + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + f"CSV parsing error in s3://{self.bucket_name}/{s3_key}: {e}", + ) + return [] + except (BotoCoreError, ClientError) as e: + error_message = ( + f"Failed to read CSV from S3: s3://{self.bucket_name}/{s3_key} - {e}" + ) + + if getattr(e, "response", {}).get("Error", {}).get("Code") == "NoSuchKey": + error_message = f"File not found: s3://{self.bucket_name}/{s3_key}" + + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, error_message + ) + return [] + + except Exception as e: + self.error_collector.add_error( + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + f"Unexpected error reading CSV from S3: {e}", + ) + return [] + + # UPLOAD METHODS + def upload_json(self, data: Any, s3_key: str, **kwargs) -> bool: + """Upload JSON data to S3. + + :param data: Data to upload (will be JSON-serialized) + :type data: Any + :param s3_key: S3 key path + :type s3_key: str + :param kwargs: Additional S3 put_object parameters + :type kwargs: dict + :return: True if upload succeeded, False otherwise + :rtype: bool + """ + try: + json_string = json.dumps( + data, + ensure_ascii=False, + separators=(",", ":"), + default=str, + ) + + default_params = { + "Bucket": self.bucket_name, + "Key": s3_key, + "Body": json_string.encode("utf-8"), + "ContentType": CONTENT_TYPE, + } + default_params.update(kwargs) + + self.s3_client.put_object(**default_params) + logger.info( + "Uploaded JSON file to path s3://%s/%s", self.bucket_name, s3_key + ) + return True + + except Exception as e: + self.error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, f"Failed to upload JSON: {e}" + ) + return False + + def upload_jsonl(self, data: List[Dict], s3_key: str, **kwargs) -> bool: + """Upload JSONL data to S3. + + :param data: List of dictionaries to upload as JSONL + :type data: List[Dict] + :param s3_key: S3 key path + :type s3_key: str + :param kwargs: Additional S3 put_object parameters + :type kwargs: dict + :return: True if upload succeeded, False otherwise + :rtype: bool + """ + try: + jsonl_lines = [] + for item in data: + jsonl_lines.append( + json.dumps( + item, ensure_ascii=False, separators=(",", ":"), default=str + ) + ) + + jsonl_string = "\n".join(jsonl_lines) + + default_params = { + "Bucket": self.bucket_name, + "Key": s3_key, + "Body": jsonl_string.encode("utf-8"), + "ContentType": "application/x-ndjson", # Standard MIME type for JSONL + } + default_params.update(kwargs) + + self.s3_client.put_object(**default_params) + logger.info( + "Uploaded JSONL file to s3://%s/%s with the length of %s records ", + self.bucket_name, + s3_key, + len(data), + ) + return True + + except Exception as e: + self.error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, f"Failed to upload JSONL: {e}" + ) + return False + + def upload_csv(self, data: List[Dict], s3_key: str, **kwargs) -> bool: + """Upload CSV data to S3. + + :param data: List of dictionaries to upload as CSV + :type data: List[Dict] + :param s3_key: S3 key path + :type s3_key: str + :param kwargs: Additional S3 put_object parameters + :type kwargs: dict + :return: True if upload succeeded, False otherwise + :rtype: bool + """ + try: + if not data: + self.error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, "No data provided to upload" + ) + return False + + df = pd.DataFrame(data) + csv_string = df.to_csv( + index=False, quoting=1, escapechar="\\", encoding="utf-8" + ) + default_params = { + "Bucket": self.bucket_name, + "Key": s3_key, + "Body": csv_string.encode("utf-8"), + "ContentType": "text/csv", # Standard type for CSV + } + default_params.update(kwargs) + + self.s3_client.put_object(**default_params) + logger.info("Uploaded CSV file to s3://%s/%s", self.bucket_name, s3_key) + return True + + except Exception as e: + self.error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, f" Failed to upload CSV: {e}" + ) + return False + + def get_sqlitedb_tables_data_from_s3(self, s3_key: str, tables_list: List[str]) -> Dict[str, List[Dict]]: + """Download SQLite DB from S3, load given tables into memory, return dict of lists. + + :param s3_key: S3 object key for the SQLite database file + :type s3_key: str + :param tables_list: List of table names to extract from the database + :type tables_list: List[str] + :return: Dictionary mapping table names to lists of row dictionaries + :rtype: Dict[str, List[Dict]] + :raises RuntimeError: If database operations fail + """ + data_store = {} + + s3_key = s3_key.lstrip("/") + logger.debug(f"Fetching SQLite tables {tables_list} from s3://{self.bucket_name}/{s3_key}") + + with tempfile.NamedTemporaryFile(delete=False) as tmp: + self.s3_client.download_fileobj(self.bucket_name, s3_key, tmp) + tmp_path = tmp.name + + try: + conn = sqlite3.connect(tmp_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + for table in tables_list: + cursor.execute(f"SELECT * FROM {table}") + data_store[table] = [dict(row) for row in cursor.fetchall()] + + conn.close() + except Exception as e: + raise RuntimeError( + f"Failed to get data from sqlite in the path of {s3_key} with error of {e}" + ) from e + finally: + os.remove(tmp_path) + + return data_store diff --git a/packages/gen/gen_ai_hub/evaluations/models/__init__.py b/packages/gen/gen_ai_hub/evaluations/models/__init__.py new file mode 100644 index 0000000..c901818 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/__init__.py @@ -0,0 +1,7 @@ +from .dataset_config import Dataset +from .evaluation_config import EvaluationConfig +from .metric_config import MetricConfig, MetricRef +from .artifact_source import ArtifactSource +from .evaluation_run import Results, EvaluationRun + +__all__ = ["Dataset", "EvaluationConfig", "MetricConfig", "MetricRef", "ArtifactSource", "Results", "EvaluationRun"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/models/artifact_source.py b/packages/gen/gen_ai_hub/evaluations/models/artifact_source.py new file mode 100755 index 0000000..f42f2dc --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/artifact_source.py @@ -0,0 +1,44 @@ +from typing import Optional, Union +from typing_extensions import Literal +from ai_api_client_sdk.models.artifact import Artifact + + +class ArtifactSource: + """ + Extends the artifact object with the relative path user can provide inside to be used for EvaluationConfig + Example Usage: + >>> ArtifactSource( + artifact={ + "id": "xyfz-rtyu-2456-ojns-yu6s", + "name": "dataset-artifact", + "url": "ai://default/eval_dataset" + ... + }, + path= "rootfolder/data.csv, + file_type="csv" + ) + >>> ArtifactSource( + artifact="xyfz-rtyu-2456-ojns-yu6s", + path="rootfolder/data.json, + file_type="json" + ) + ) + """ + + def __init__( + self, + file_type: Literal["csv", "json", "jsonl"], + artifact: Union[str,Artifact], # str is id + path: Optional[str] = None, + ): + """ + Parameters: + artifact(Union[str,Artifact]): Can just provide the artifact id as a string or the Artifact object of the AI_API_Client sdk. + path(Optional[str]): Relative path within the artifact path provided and should point to a single file. + file_type(Literal["csv", "json", "jsonl"]): One of the supported file_types + """ + self.artifact = artifact + self.path = path + self.file_type = file_type + +__all__ = ["ArtifactSource"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/models/dataset_config.py b/packages/gen/gen_ai_hub/evaluations/models/dataset_config.py new file mode 100644 index 0000000..d09062d --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/dataset_config.py @@ -0,0 +1,80 @@ +from pathlib import Path +from typing import Union, Optional +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.constants import ( + SUFFIX_TO_FILE_TYPE, +) + + +class Dataset: + """Dataset object for the evaluations flow. + + The Dataset class accepts various source types for evaluation datasets including + local file paths (as strings or Path objects) or AI Core artifacts. + + :param source: Source of the dataset - can be a file path string, Path object, or ArtifactSource + :type source: Union[str, Path, ArtifactSource] + + **Examples**: + + Using a Path object: + + >>> Dataset(Path("data/sample.json")) + + Using a string path: + + >>> Dataset("data/sample.json") + + Using an ArtifactSource with artifact dictionary: + + >>> Dataset( + ... ArtifactSource( + ... artifact={ + ... "id": "xyfz-rtyu-2456-ojns-yu6s", + ... "name": "dataset-artifact", + ... "url": "ai://default/eval_dataset" + ... }, + ... path="rootfolder/data.csv", + ... file_type="csv" + ... ) + ... ) + + Using an ArtifactSource with artifact ID: + + >>> Dataset( + ... ArtifactSource( + ... artifact="xyfz-rtyu-2456-ojns-yu6s", + ... path="rootfolder/data.csv", + ... file_type="csv" + ... ) + ... ) + """ + + def __init__(self, source: Union[str, Path, ArtifactSource]): + """Initialize a Dataset instance. + + :param source: Source of the dataset - can be a file path string, Path object, or ArtifactSource + :type source: Union[str, Path, ArtifactSource] + """ + self.source = source + + @property + def file_type(self) -> Optional[str]: + """Infer the file type from the source. + + For ArtifactSource, returns the explicitly set file_type. + For file paths, infers the type from the file extension. + + :return: File type (e.g., "json", "jsonl", "csv") or None if cannot be determined + :rtype: Optional[str] + """ + if isinstance(self.source, ArtifactSource): + return self.source.file_type + + # If source is a path or string, try inferring from file extension + path_str = str(self.source) if isinstance(self.source, Path) else self.source + suffix = Path(path_str).suffix.lower() + + return SUFFIX_TO_FILE_TYPE.get(suffix, None) + +__all__ = ["Dataset"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/models/evaluation_config.py b/packages/gen/gen_ai_hub/evaluations/models/evaluation_config.py new file mode 100644 index 0000000..2de003a --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/evaluation_config.py @@ -0,0 +1,125 @@ +from typing import List, Optional, Union +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.metric_config import MetricConfig + +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from gen_ai_hub.orchestration_v2.models.template_ref import ( + TemplateRef, +) +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec + + +class EvaluationConfig: + """Defines the evaluation configuration object for the Evaluations flow. + + This class encapsulates all configuration parameters needed to run an evaluation job, + including the model/template configuration, dataset, metrics, and execution settings. + + At least one of the following must be provided: + + - ``llm`` and ``template`` combination (using orchestration_v2 models) + - ``orchestration_registry_reference`` (UUID of a registered orchestration configuration) + + :param dataset_config: Dataset configuration object specifying the evaluation dataset + :type dataset_config: Dataset + :param metrics: List of metric configurations for evaluation + :type metrics: List[MetricConfig] + :param llm: LLM configuration from orchestration_v2 (LLMModelDetails) + :type llm: Optional[LLM] + :param template: Prompt template as string, PromptTemplateSpec, or TemplateRef + :type template: Optional[Union[str, PromptTemplateSpec, TemplateRef]] + :param orchestration_registry_reference: UUID of registered orchestration configuration + :type orchestration_registry_reference: Optional[str] + :param template_variable_mapping: Variable mapping for the prompt template + :type template_variable_mapping: Optional[dict] + :param test_row_count: Number of rows to sample from dataset (-1 for all rows), defaults to -1 + :type test_row_count: Optional[int] + :param repetitions: Number of times to repeat evaluation over the dataset, defaults to 1 + :type repetitions: Optional[int] + :param tags: User-defined metadata as key-value pairs, defaults to "{}" + :type tags: Optional[dict] + :param debug_mode: Enable debug logs in hyperscaler output path, defaults to False + :type debug_mode: Optional[bool] + + .. note:: + This module uses orchestration_v2 models directly. + + **Example using TemplateRef with ID**: + + >>> from gen_ai_hub.evaluations.models import EvaluationConfig, Dataset, MetricConfig + >>> from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM + >>> from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID + >>> config = EvaluationConfig( + ... dataset_config=Dataset("data/test.jsonl"), + ... metrics=[MetricConfig(name="accuracy")], + ... llm=LLM(name="gpt-4", version="latest"), + ... template=TemplateRef(template_ref=TemplateRefByID(id="template-id-here")), + ... test_row_count=100 + ... ) + + **Example using TemplateRef with scenario/name/version**: + + >>> from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRefByScenarioNameVersion + >>> config = EvaluationConfig( + ... dataset_config=Dataset("data/test.jsonl"), + ... metrics=[MetricConfig(name="accuracy")], + ... llm=LLM(name="gpt-4", version="latest", params={"temperature": 0.7}), + ... template=TemplateRef(template_ref=TemplateRefByScenarioNameVersion( + ... scenario="foundation-models", name="prompt1", version="1.0" + ... )), + ... test_row_count=100 + ... ) + """ + + def __init__( + self, + dataset_config: Dataset, + metrics: List[MetricConfig], + llm: Optional[LLM] = None, + template: Optional[Union[str, PromptTemplateSpec, TemplateRef]] = None, + orchestration_registry_reference: Optional[ + str + ] = None, # currently only supports uuid, in future when dataclasses comes up in sdk, then support scenario/name/version + template_variable_mapping: Optional[dict] = None, + test_row_count: Optional[int] = -1, + repetitions: Optional[int] = 1, + tags: Optional[dict] = "{}", + debug_mode: Optional[bool] = False, + ): + """Initialize an EvaluationConfig instance. + + :param dataset_config: Dataset configuration object + :type dataset_config: Dataset + :param metrics: List of metric configurations + :type metrics: List[MetricConfig] + :param llm: LLM object from orchestration_v2 (LLMModelDetails), defaults to None + :type llm: Optional[LLM] + :param template: Prompt template (string, PromptTemplateSpec, or TemplateRef), defaults to None + :type template: Optional[Union[str, PromptTemplateSpec, TemplateRef]] + :param orchestration_registry_reference: UUID of orchestration config, defaults to None + :type orchestration_registry_reference: Optional[str] + :param template_variable_mapping: Variable mapping for prompt template, defaults to None + :type template_variable_mapping: Optional[dict] + :param test_row_count: Number of dataset rows to sample (-1 for all), defaults to -1 + :type test_row_count: Optional[int] + :param repetitions: Number of evaluation repetitions (minimum: 1), defaults to 1 + :type repetitions: Optional[int] + :param tags: Key-value metadata pairs applied to all runs, defaults to "{}" + :type tags: Optional[dict] + :param debug_mode: Enable debug logging, defaults to False + :type debug_mode: Optional[bool] + :raises ValueError: If neither (llm, template) nor orchestration_registry_reference is provided + """ + super().__init__() + self.llm = llm + self.template = template + self.orchestration_registry_reference = orchestration_registry_reference + self.template_variable_mapping = template_variable_mapping + self.dataset_config = dataset_config + self.metrics = metrics + self.test_row_count = test_row_count + self.tags = tags + self.repetitions = repetitions + self.debug_mode = debug_mode + +__all__ = ["EvaluationConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/models/evaluation_run.py b/packages/gen/gen_ai_hub/evaluations/models/evaluation_run.py new file mode 100644 index 0000000..342af5e --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/evaluation_run.py @@ -0,0 +1,467 @@ +import uuid +import re +import pandas as pd +from typing import Optional, Any, List +from dataclasses import dataclass +from ai_api_client_sdk.models.status import Status +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.tracking.tracking import Tracking +from gen_ai_hub.evaluations.utils.aicore_utils import wait_for_target_status +from gen_ai_hub.evaluations.constants import ( + ADDITIONAL_INFO_KEY, + DEFAULT_KEY, + AWS_OSS_PATH_PREFIX_URL_KEY, + DEFAULT_TIMEOUT, + RESULTS_FILE_KEY, + AWS_OSS_BUCKET_URL_KEY, + AWS_OSS_REGION_URL_KEY, + COMPLETIONS_TABLE_KEY, + METRICS_TABLE_KEY, +) +from gen_ai_hub.evaluations.helpers.s3_file_client import S3FileClient +from gen_ai_hub.evaluations._internal._models import ( + _AWSObjectStoreData, +) + + +@dataclass +class ExecutionStatusDetails: + """Dataclass for execution status details. + + :param details: Detailed information about the execution status + :type details: Any + :param status: Current status of the execution + :type status: Any + """ + details: Any + status: Any + + +class _RunContext: + """Holds the metadata context information of the EvaluationRun object. + + :param execution_id: ID of the AI Core execution + :param configuration_id: ID of the configuration + :param artifact_id: ID of the artifact + :param ai_core_client: AI Core client instance + :param resource_group: Resource group name + :param object_store_credentials: Object store credentials + :param metrics_list: List of metrics to evaluate + :param cached_results_data: Cached results data, defaults to None + """ + + def __init__( + self, + execution_id, + configuration_id, + artifact_id, + ai_core_client, + resource_group, + object_store_credentials, + metrics_list, + cached_results_data=None, + ): + self.execution_id = execution_id + self.configuration_id = configuration_id + self.artifact_id = artifact_id + self.ai_core_client = ai_core_client + self.resource_group = resource_group + self.object_store_credentials = object_store_credentials + self.metrics_list = metrics_list + self.cached_results_data = cached_results_data + + +class EvaluationRun: + """Represents an individual EvaluationRun object and its associated context. + + :param run_id: Unique identifier for the evaluation run + :type run_id: str + :param execution_id: ID of the AI Core execution + :type execution_id: str + :param ai_core_client: AI Core client instance + :type ai_core_client: AICoreV2Client + :param configuration_id: ID of the configuration, defaults to None + :type configuration_id: str + :param artifact_id: ID of the artifact, defaults to None + :type artifact_id: str + :param resource_group: Resource group name, defaults to None + :type resource_group: str + :param object_store_credentials: Object store credentials, defaults to None + :type object_store_credentials: _AWSObjectStoreData + :param metrics_list: List of metrics to evaluate, defaults to None + :type metrics_list: List[str] + """ + + # Step-level error messages for failed pods + _STEP_ERROR_MESSAGES = { + "combine": "Evaluation job failed in Aggregating Results step.", + "completion": "Evaluation job failed in generating Completion Responses step.", + "config": "Evaluation job failed in Config Validation step.", + } + + def __init__( + self, + run_id: str, + execution_id: str, + ai_core_client: AICoreV2Client, + configuration_id: str = None, + artifact_id: str = None, + resource_group: str = None, + object_store_credentials: _AWSObjectStoreData = None, + metrics_list: List[str] = None, + ): + self.id = run_id + self.status = Status.UNKNOWN + self._run_context = _RunContext( + # RUNTIME_INFO OBJECT ISOLATION + execution_id=execution_id, + configuration_id=configuration_id, + artifact_id=artifact_id, + ai_core_client=ai_core_client, + resource_group=resource_group, + object_store_credentials=object_store_credentials, + metrics_list=metrics_list, + cached_results_data=None, + ) + + def set_cached_results_data(self, data): + """Set the cached results data from the child results class. + + :param data: Results data to cache + :type data: Any + """ + self._cached_results_data = data + + def _execution_status_fetcher(self): + return self._run_context.ai_core_client.execution.get( + execution_id=self._run_context.execution_id, + resource_group=self._run_context.resource_group, + select="status", + ) + + def wait_for_completion(self, timeout: Optional[int] = None): + """Wait for the evaluation run to complete by polling status. + + :param timeout: Maximum time to wait in seconds, defaults to 3600 (1 hour) + :type timeout: Optional[int] + """ + + wait_for_target_status( + status_fetcher=self._execution_status_fetcher, + target_status=Status.COMPLETED, + timeout=timeout or DEFAULT_TIMEOUT, # one hour + ) + return + + def get_current_status(self): + """Get the current status of the evaluation run. + + :return: Current status of the run + :rtype: Status + :raises ValueError: If failed to retrieve the current status + """ + try: + response = self._execution_status_fetcher() + return response.status + + except Exception as e: + raise ValueError( + f"Failed to get the current status of the run with the error of {e}" + ) + + # for now mention that everything is run_id not specific and will come up in future. + # User might be interested in run_id specific logs. + def get_debug_info(self) -> ExecutionStatusDetails: + """Provide debug information when execution status is FAILED or DEAD. + + :return: Execution status details including failed pod information + :rtype: ExecutionStatusDetails + """ + execution_status_response = self._execution_status_fetcher() + current_status = execution_status_response.status + status_details = getattr(execution_status_response, "status_details", None) + + if not status_details: + return ExecutionStatusDetails( + details=( + "No specific details found. Please use get_debug_logs() " + "function to get more information!" + ), + status=current_status, + ) + + failed_pod_details = self._extract_failed_pods(status_details) + workflow_lookup = self._build_workflow_lookup(status_details) + + self._enrich_failed_pods( + failed_pod_details, + workflow_lookup, + ) + + return ExecutionStatusDetails( + details=failed_pod_details, + status=current_status, + ) + + def _extract_failed_pods(self, status_details: dict) -> list[dict]: + details = status_details.get("details", []) + + return [ + { + "name": c["pod_name"], + "last_log_message": c["last_log_messages"], + } + for c in details + if c.get("exit_code", 0) > 0 + ] + + def _build_workflow_lookup(self, status_details: dict) -> dict: + workflow_lookup = {} + + for workflow in status_details.get("workflow_info", []): + workflow_id = workflow.get("id", "") + suffix = workflow_id.split("-")[-1] + workflow_lookup[suffix] = workflow + + return workflow_lookup + + def _enrich_failed_pods( + self, + failed_pods: list[dict], + workflow_lookup: dict, + ) -> None: + for pod in failed_pods: + pod_name = pod.get("name", "") + + if self._apply_step_level_message(pod, pod_name): + continue + + self._apply_metric_level_message( + pod, + pod_name, + workflow_lookup, + ) + + def _apply_step_level_message( + self, + pod: dict, + pod_name: str, + ) -> bool: + for key, message in self._STEP_ERROR_MESSAGES.items(): + if key in pod_name: + pod[ADDITIONAL_INFO_KEY] = message + return True + return False + + def _apply_metric_level_message( + self, + pod: dict, + pod_name: str, + workflow_lookup: dict, + ) -> None: + suffix = pod_name.split("-")[-1] + workflow = workflow_lookup.get(suffix) + + if not workflow: + return + + metric_name = self._extract_metric_name(workflow.get("name", "")) + message = workflow.get("message", "Unknown error") + + pod[ADDITIONAL_INFO_KEY] = ( + f"Evaluation job failed in metric evaluation for " + f"'{metric_name}' with error: {message}" + ) + + def _extract_metric_name(self, name: str) -> str | None: + match = re.search(r"metric_name:([^,)\s]+(?: [^,)\s]+)*)", name) + return match.group(1) if match else None + + + def get_debug_logs(self): + """Get the complete trace of execution logs. + + :return: List of log entries as dictionaries + :rtype: list + """ + execution_logs = self._run_context.ai_core_client.execution.query_logs( + execution_id=self._run_context.execution_id + ) + log_items = execution_logs.data.result + log_values = [item.__dict__ for item in log_items] + return log_values + + def results(self): + """Get the results of the evaluation run. + + :return: Results object for accessing completion and metric results + :rtype: Results + :raises ValueError: If execution is not completed + """ + # validate if execution is actually completed + execution_status = self._execution_status_fetcher().status + if execution_status != Status.COMPLETED: + if execution_status == Status.RUNNING: + raise ValueError( + "status of the run is Running, Use wait_for_completion() to wait until completed and then see the results" + ) + else: + raise ValueError( + f"Cannot call results as the status of run is not yet completed. The current status is {execution_status}" + ) + + return Results(self) + + def load_results_tables(self): + """Download results from S3 and load the required table data. + + :return: Dictionary containing completions and metrics table data + :rtype: dict + :raises RuntimeError: If failed to download results + """ + try: + if self._run_context.cached_results_data: + return self._run_context.cached_results_data + + secret_response = self._run_context.ai_core_client.object_store_secrets.get( + name=DEFAULT_KEY, resource_group=self._run_context.resource_group + ) + metadata_details = secret_response.metadata + + if isinstance( + self._run_context.object_store_credentials, _AWSObjectStoreData + ): + secret_path_prefix = metadata_details.get(AWS_OSS_PATH_PREFIX_URL_KEY) + aws_bucket_name = metadata_details.get(AWS_OSS_BUCKET_URL_KEY) + aws_region = metadata_details.get(AWS_OSS_REGION_URL_KEY) + s3_file_client = S3FileClient( + aws_bucket_name, + aws_region, + self._run_context.object_store_credentials.aws_access_key_id, + self._run_context.object_store_credentials.aws_secret_access_key, + ) + results_db_file_key = f"{secret_path_prefix}/{self._run_context.execution_id}/tmp/{RESULTS_FILE_KEY}" + tables_to_load = [ + COMPLETIONS_TABLE_KEY, + METRICS_TABLE_KEY, + ] + data = s3_file_client.get_sqlitedb_tables_data_from_s3( + results_db_file_key, tables_to_load + ) + return data + except Exception as e: + raise RuntimeError( + f"Could not download the results for the run id :{self.id} failing with: ", + e, + ) from e + + +def configure_pandas_display(): + pd.set_option("display.max_columns", None) + pd.set_option("display.max_rows", None) + pd.set_option("display.max_colwidth", None) + pd.set_option("display.expand_frame_repr", False) + + +class Results: + """Represents the Results handler for an EvaluationRun object. + + This class provides methods to access completion results, metric results, + and aggregated results for a specific evaluation run. + + :param run: The parent EvaluationRun object + :type run: EvaluationRun + """ + + def __init__(self, run: EvaluationRun): + self.run = run + self._data_store = None + self._run_context = self.run._run_context + self._tracking_client = Tracking( + base_url=self._run_context.ai_core_client.base_url, + token_creator=self._run_context.ai_core_client.rest_client.get_token, + resource_group=self._run_context.resource_group, + ) + configure_pandas_display() + + def _ensure_loaded(self): + """Fetch and store results data if not already loaded. + + Lazy-loads the table data only when results are accessed. + """ + if self._data_store is None: + self._data_store = self.run.load_results_tables() + self.run.set_cached_results_data(self._data_store) + + def _filter_by_run_id(self, data: List[dict], run_id): + """Filter data to return only rows for the specified run_id. + + :param data: List of data dictionaries to filter + :type data: List[dict] + :param run_id: Run ID to filter by + :type run_id: str + :return: Filtered list containing only rows matching the run_id + :rtype: list + """ + run_id = uuid.UUID( + run_id + ).hex + filtered_store = [ + current_row for current_row in data if current_row.get("run_id") == run_id + ] + return filtered_store + + def completions(self): + """Get the completion results for the run. + + :return: DataFrame containing completion results for the run + :rtype: pd.DataFrame + :raises ValueError: If error occurs while fetching completions + """ + try: + self._ensure_loaded() + filtered_data = self._filter_by_run_id( + self._data_store[COMPLETIONS_TABLE_KEY], self.run.id + ) + df = pd.DataFrame(filtered_data) + return df + except Exception as e: + raise ValueError("Error while trying to fetch completions: ", e) from e + + def metrics(self): + """Get the metric-level results for the run. + + :return: DataFrame containing metric results for the run + :rtype: pd.DataFrame + :raises ValueError: If error occurs while fetching metric results + """ + try: + self._ensure_loaded() + run_id_filtered_rows = self._filter_by_run_id( + self._data_store[METRICS_TABLE_KEY], self.run.id + ) + df = pd.DataFrame(run_id_filtered_rows) + return df + except Exception as e: + raise ValueError("Error while trying to fetch metric results: ", e) from e + + def aggregations(self): + """Get the aggregated results for the run from the tracking service. + + :return: JSON response containing aggregated metric results + :rtype: dict + :raises ValueError: If error occurs while fetching aggregation results + """ + try: + run_id = self.run.id + run_id = uuid.UUID(run_id).hex + + response = self._tracking_client.query(execution_ids=[run_id]) + return response + except Exception as e: + raise ValueError( + "Error while trying to fetch aggregation results from tracking service with error of: ", + e, + ) from e + +__all__ = ["EvaluationRun", "Results"] diff --git a/packages/gen/gen_ai_hub/evaluations/models/metric_config.py b/packages/gen/gen_ai_hub/evaluations/models/metric_config.py new file mode 100644 index 0000000..7745c25 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/models/metric_config.py @@ -0,0 +1,40 @@ +class MetricRef: + """ + Represents a reference to a specific metric definition. + + A metric can be identified in multiple ways: + - By its UUID from metric management service (`id`) + - By name (`name`) + - By a combination of scenario, name, and version (`scenario`, `name`, `version`) + """ + + def __init__( + self, + scenario: str = None, + name: str = None, + version: str = None, + id: str = None, + ): + self.name = name + self.scenario = scenario + self.version = version + self.id = id + +class MetricConfig: + """ + Defines the metric config of the evaluation flow + + Parameters: + reference(MetricRef): Provide the reference of metric to be evaluated, can be one of name,uuid(id), scenario/name/version + variable_mapping(Optional[dict]): Any variable maping associated with the metric + """ + + def __init__( + self, + reference: MetricRef, + variable_mapping: dict = None, + ): + self.reference = reference + self.variable_mapping = variable_mapping + +__all__ = ["MetricRef", "MetricConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/utils/__init__.py b/packages/gen/gen_ai_hub/evaluations/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/evaluations/utils/aicore_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/aicore_utils.py new file mode 100644 index 0000000..f46da40 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/aicore_utils.py @@ -0,0 +1,877 @@ +import time +import json +import uuid +import os +from pathlib import PurePosixPath +from typing import List, Union, Any, Optional, Callable, Dict +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from ai_core_sdk.models.object_store_secret import ObjectStoreSecret +from ai_api_client_sdk.models.configuration import Configuration +from ai_api_client_sdk.models.deployment import Deployment +from ai_api_client_sdk.models.status import Status +from ai_api_client_sdk.models.artifact import Artifact +from ai_api_client_sdk.models.input_artifact_binding import InputArtifactBinding +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from gen_ai_hub.evaluations.constants import ( + ORCHESTRATION_GLOBAL_SCENARIO_NAME, + AI_PROTOCOL_PREFIX, + AWS_OSS_BUCKET_URL_KEY, + AWS_OSS_REGION_URL_KEY, + AWS_OSS_PATH_PREFIX_URL_KEY, + CSV_FILE_TYPE, + JSON_FILE_TYPE, + EVALUATIONS_SCENARIO_ID, + EVALUATIONS_CONFIG_PREFIX_KEY, + EVALUATIONS_ARTIFACT_PREFIX_KEY, + DATASET_FOLDER_KEY, + EVAL_ORCHESTRATION_CONFIG_PREFIX_NAME, + EVALUATIONS_ARTIFACT_DESCRIPTION, + ORCHESTRATION_REGISTRY_ENDPOINT, + SYSTEM_DEFINED_METRIC_MAPPING, +) +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.utils.oss_secret_utils import ( + fetch_object_store_secret_by_name, +) +from gen_ai_hub.evaluations.helpers.s3_file_client import S3FileClient +from gen_ai_hub.evaluations._internal._models import _AWSObjectStoreData +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData +from gen_ai_hub.evaluations.utils.metric_client_utils import ( + get_metric_template_info_from_server, + get_metric_version_history, + get_custom_metric_by_id, +) +from gen_ai_hub.evaluations.models.metric_config import MetricConfig +from gen_ai_hub.evaluations.helpers.logging import get_logger +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig + + +logger = get_logger() + + +def generate_random_id(): + """generates and returns a random uuid everytime""" + return uuid.uuid4().hex + + +def find_configuration_id_by_name( + configurations_list: List[Configuration], target_name: str +): + response = next( + ( + configuration.id + for configuration in configurations_list + if configuration.name == target_name + ), + None, + ) + logger.info("value of id fetched for configuration is %s", response) + return response + + +def get_all_configurations( + ai_core_client: AICoreV2Client, resource_group: str, scenario_id: str +) -> List[Configuration]: + try: + response = ai_core_client.configuration.query( + scenario_id=scenario_id, resource_group=resource_group + ) + return response.resources # direct list of resources + except Exception as e: + raise ValueError( + f"Could not fetch all configurations with the error of {e}" + ) from e + + +def get_running_deployments_by_configuration_id( + ai_core_client: AICoreV2Client, configuration_id: str, resource_group: str +) -> List[Deployment]: + try: + response = ai_core_client.deployment.query( + scenario_id=ORCHESTRATION_GLOBAL_SCENARIO_NAME, + configuration_id=configuration_id, + status=Status.RUNNING, + resource_group=resource_group, + ) + return response.resources + except Exception as e: + raise ValueError( + f"Could not fetch all deployments with the error of {e}" + ) from e + + +def create_deployment_by_configuration_id( + ai_core_client: AICoreV2Client, configuration_id: str, resource_group: str +): + try: + # create deployment + deployment_response = ai_core_client.deployment.create( + configuration_id=configuration_id, + resource_group=resource_group, + ) + + # wait till the deployment is up and running + def deployment_status_fetcher(): + return ai_core_client.deployment.get( + deployment_id=deployment_response.id, + resource_group=resource_group, + ) + + def extract_deployment_url(response): + return response.deployment_url + + deployment_url = wait_for_target_status( + status_fetcher=deployment_status_fetcher, + target_status=Status.RUNNING, + extract_url=extract_deployment_url, + ) + if ( + deployment_url is None + ): # when the value is returned after waiting till its running + raise ValueError( + "Deployment URL could not reach the target status of Running, so failing the setup!" + ) + + return deployment_url + except Exception as e: + raise RuntimeError( + f"Creation of orchestration deployment failed with the error of {e}" + ) from e + + +def create_llm_orchestration_deployment_url( + ai_core_client: AICoreV2Client, resource_group: str +): + """creates the llm-orchestration configuration based on orchestration global scenario and then creates a deployment using that configuration""" + try: + # create config. + configuration_name = ( + EVAL_ORCHESTRATION_CONFIG_PREFIX_NAME + generate_random_id()[:5] + ) + logger.info( + "Using the configuration name of %s to create the AICore configuration", + configuration_name, + ) + configuration_response = ai_core_client.configuration.create( + name=configuration_name, # some randomization maybe here. + scenario_id=ORCHESTRATION_GLOBAL_SCENARIO_NAME, + executable_id=ORCHESTRATION_GLOBAL_SCENARIO_NAME, + ) + logger.info( + "Response after creating the configuration is %s", configuration_response + ) + + # positive case gets id + configuration_id = configuration_response.id + return create_deployment_by_configuration_id( + ai_core_client, configuration_id, resource_group + ) + + except Exception as e: + raise RuntimeError( + f"Creation of orchestration deployment url failed with the error of {e}" + ) from e + + +def wait_for_target_status( + status_fetcher: Callable[[], Any], + target_status: Status, + extract_url: Optional[Callable[[Any], str]] = None, + timeout: int = 1200, + initial_interval: int = 120, + pending_interval: int = 40, +) -> Optional[str]: + """Reusable polling function to wait until a resource reaches target_status. + + :param status_fetcher: Function to get current status response + :type status_fetcher: Callable[[], Any] + :param target_status: Target status to wait for (Status enum) + :type target_status: Status + :param extract_url: Optional function to extract URL from response, defaults to None + :type extract_url: Optional[Callable[[Any], str]] + :param timeout: Maximum time to wait in seconds, defaults to 1200 + :type timeout: int + :param initial_interval: Initial polling interval in seconds, defaults to 120 + :type initial_interval: int + :param pending_interval: Polling interval for pending/running status in seconds, defaults to 40 + :type pending_interval: int + :return: Extracted URL if extract_url is provided and status reached, None otherwise + :rtype: Optional[str] + """ + logger.info("Waiting for the target end status of %s", target_status) + try: + start = time.time() + current_interval = initial_interval + + while time.time() - start < timeout: + response = status_fetcher() + status = response.status + logger.info("Current status is : %s", status) + + if status == target_status: + end = time.time() + logger.info( + "Time in wait till it reached target_status of %s is %s seconds", + target_status, + end - start, + ) + if extract_url: + return extract_url(response) + return None # or return success indication + + elif status == Status.UNKNOWN: + current_interval = initial_interval + elif ( + status == Status.PENDING or status == Status.RUNNING + ): # Running status is also intermediate status to be reused for executions + current_interval = pending_interval + elif ( + status == Status.DEAD + or status == Status.STOPPED + or status == Status.STOPPING + ): + logger.error( + "Could not reach the target status. Please use debug_info function to get the info regarding failures" + ) + return None + + time.sleep(current_interval) + + except Exception as e: + raise KeyError( + f"Failed to reach the target status of {target_status} because of {e}" + ) from e + + logger.error("Timeout reached without success status.") + return None + + +def read_data_from_artifact( + object_store_credentials: Union[ + _AWSObjectStoreData + ], # can be later extend to other providers + object_store_secret_metadata_details: Dict[str, str], + s3_file_key: str, + file_type: str, + error_collector: ValidationCollector, +): + file_data = [] + if isinstance(object_store_credentials, _AWSObjectStoreData): + s3_file_client = S3FileClient( + object_store_secret_metadata_details.get(AWS_OSS_BUCKET_URL_KEY), + object_store_secret_metadata_details.get(AWS_OSS_REGION_URL_KEY), + object_store_credentials.aws_access_key_id, + object_store_credentials.aws_secret_access_key, + error_collector=error_collector, + ) + if file_type == CSV_FILE_TYPE: + file_data = s3_file_client.read_csv(s3_file_key) + elif file_type == JSON_FILE_TYPE: + file_data = s3_file_client.read_json(s3_file_key) + else: + file_data = s3_file_client.read_jsonl(s3_file_key) + + return file_data + + +def build_s3_file_key( + object_store_secret_metadata_details: Dict[str, str], + artifact_url_relative_path: str, + artifact_source: ArtifactSource, +): + path_prefix = object_store_secret_metadata_details.get(AWS_OSS_PATH_PREFIX_URL_KEY) + final_path = [] + + if path_prefix: + final_path.append(path_prefix) + + if artifact_url_relative_path: + final_path.append(artifact_url_relative_path) + + if artifact_source.path is not None: + final_path.append(artifact_source.path) + + if not final_path: + return "" + + # PurePosixPath ensures forward slashes (S3-compatible) + return str(PurePosixPath(*final_path)) + + +# Assumption is the provided artifact is the same as what creds are provided and url is of type ai://secret_name/pathPRefix +def resolve_artifact_path( + artifact_source: ArtifactSource, + ai_core_client: AICoreV2Client, + object_store_credentials: Union[_AWSObjectStoreData], + resource_group: str, + error_collector: ValidationCollector, +): + file_type = artifact_source.file_type + artifact = artifact_source.artifact + if isinstance(artifact, str): + artifact = ai_core_client.artifact.get(artifact_id=artifact) + artifact_url = artifact.url + if artifact_url.startswith(AI_PROTOCOL_PREFIX): + url_part = artifact_url[len(AI_PROTOCOL_PREFIX) :] + + sep = url_part.find("/") + if sep == -1: # "/" does not exist in url_part + error_collector.add_error( + ErrorCode.INVALID_ARTIFACT_URL_ERROR, + f"Artifact URL '{artifact_url}' has invalid format. Valid format is {AI_PROTOCOL_PREFIX}/", + ) + return [] + + object_store_secret_name = url_part[:sep] + artifact_url_relative_path = url_part[sep + 1 :] + object_store_secret_details: ObjectStoreSecret = ( + fetch_object_store_secret_by_name( + ai_core_client, object_store_secret_name, resource_group, error_collector + ) + ) + if object_store_secret_details is None: + error_collector.add_error( + ErrorCode.INVALID_OBJECT_STORE_SECRET_ERROR, + f"The provided artifact url's object store secret {object_store_secret_name} is invalid. Please provide a valid url", + ) + return [] + + # building the key of where the file is present based on artifact url and path_prefix given in secret + # order of resolution is secret pathprefix + artifact url continuation after secret name + relative path inside ArtifactSource object + s3_file_key = build_s3_file_key( + object_store_secret_details.metadata, + artifact_url_relative_path, + artifact_source, + ) + + file_data = read_data_from_artifact( + object_store_credentials, + object_store_secret_details.metadata, + s3_file_key, + file_type, + error_collector, + ) + return file_data + return [] + + +def fetch_deployment_config( + deployment_id: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + try: + deployment_config = ai_core_client.deployment.get(deployment_id, resource_group) + return deployment_config + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Could not fetch configuration of the deployment id {deployment_id} failing with the error of {e}", + ) + return [] + + +def fetch_configuration_by_id( + configuration_id: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + try: + configuration_response = ai_core_client.configuration.get( + configuration_id, resource_group=resource_group + ) + return configuration_response + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Could not fetch configuration of the configuration id {configuration_id} failing with the error of {e}", + ) + return [] + + +def call_orchestration_service_with_v2_config( + test_orch_config: dict, + ai_core_client: AICoreV2Client, + orchestration_deployment_url: str, + resource_group: str, + error_collector: ValidationCollector, + proxy_client=None, +): + try: + # Create the orchestration service with the deployment URL + orchestration_service = OrchestrationService( + api_url=orchestration_deployment_url, + proxy_client=proxy_client, + ) + + # Convert the dict config to OrchestrationConfig object + orch_config = OrchestrationConfig(**test_orch_config) + + # Make the completion call using the orchestration_v2 client + response = orchestration_service.run(config=orch_config) + + # If we get here, the call succeeded + return response + + except Exception as e: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + f"Error occurred: {e} while trying to run the test orchestration config endpoint call with this user provided deployment url of {orchestration_deployment_url}", + ) + + +def upload_file_to_aws_s3( + object_store_credentials: Union[_AWSObjectStoreData], + object_store_secret_metadata_details: Dict[str, str], + file_data: Any, + file_key: str, + file_type: str, + error_collector: ValidationCollector, +): + s3_file_client = S3FileClient( + object_store_secret_metadata_details.get(AWS_OSS_BUCKET_URL_KEY), + object_store_secret_metadata_details.get(AWS_OSS_REGION_URL_KEY), + object_store_credentials.aws_access_key_id, + object_store_credentials.aws_secret_access_key, + error_collector=error_collector, + ) + + if file_type == CSV_FILE_TYPE: + return s3_file_client.upload_csv(file_data, file_key) + + elif file_type == JSON_FILE_TYPE: + return s3_file_client.upload_json(file_data, file_key) + # not csv and json trying jsonl + return s3_file_client.upload_jsonl(file_data, file_key) + + +def upload_evaluation_dataset_data( + evaluation_config_data: _EvaluationConfigData, + object_store_credentials: Union[_AWSObjectStoreData], + object_store_secret_name: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + """Method to upload the evaluation config data using the object store secrets data passed""" + # creating a unique uuid name for the rootfolder. + artifact_folder_path = generate_random_id() + root_folder_id = artifact_folder_path + logger.info("Randomly created root folder id created is %s", artifact_folder_path) + logger.info( + "Using Object Store Secret %s to upload the files from the config provided", + object_store_secret_name, + ) + object_store_secret_details: ObjectStoreSecret = fetch_object_store_secret_by_name( + ai_core_client, object_store_secret_name, resource_group, error_collector + ) + + aicore_configuration_dataset_file_path = "" + + if isinstance(object_store_credentials, _AWSObjectStoreData): + path_prefix_key = object_store_secret_details.metadata.get( + AWS_OSS_PATH_PREFIX_URL_KEY + ) + if path_prefix_key: + root_folder_id = os.path.join(path_prefix_key, root_folder_id) + logger.info( + "Updated rootfolder after incorporating pathPrefix value from secret is %s", + root_folder_id, + ) + + # uploading the testdataset to testdataset folder + dataset_folder = os.path.join(root_folder_id, DATASET_FOLDER_KEY) + dataset_data = evaluation_config_data.dataset_data + dataset_type = evaluation_config_data.dataset_type + dataset_file_name = f"{generate_random_id()[:7]}.{dataset_type}" # taking only 7chars from the generated random id. + + dataset_file_key = os.path.join(dataset_folder, dataset_file_name) + aicore_configuration_dataset_file_path = os.path.join( + DATASET_FOLDER_KEY, dataset_file_name + ) + + dataset_file_uploaded = upload_file_to_aws_s3( + object_store_credentials, + object_store_secret_details.metadata, + dataset_data, + dataset_file_key, + dataset_type, + error_collector, + ) + if not dataset_file_uploaded: + # stop uploading files if any of the file is not uploaded so to not waste the compute resources + error_collector.add_error( + ErrorCode.FILE_UPLOAD_ERROR, + f"Error uploading testdataset to the object store secret with the folder path of {dataset_file_key}", + ) + return "", "" + + return ( + artifact_folder_path, + aicore_configuration_dataset_file_path, + ) + + +def register_aicore_artifact( + artifact_folder_path: str, + ai_core_client: AICoreV2Client, + resource_group: str, + object_store_secret_name: str, + error_collector: ValidationCollector, +): + input_artifact_path = os.path.join( + AI_PROTOCOL_PREFIX, object_store_secret_name, artifact_folder_path + ) + random_id = generate_random_id() + artifact_name = EVALUATIONS_ARTIFACT_PREFIX_KEY + random_id[:7] + logger.info("Artifact path of upload is %s", input_artifact_path) + logger.info("Artifact file name is %s", artifact_name) + + try: + response = ai_core_client.artifact.create( + name=artifact_name, + kind=Artifact.Kind.OTHER, + url=input_artifact_path, + scenario_id=EVALUATIONS_SCENARIO_ID, + resource_group=resource_group, + description=EVALUATIONS_ARTIFACT_DESCRIPTION, + ) + artifact_id = response.id + logger.info("result of artifact creation is %s", artifact_id) + return artifact_id + except Exception as e: + error_collector.add_error( + ErrorCode.ARTIFACT_CREATION_FAILURE, + f"Error occurred while attempting to create an artifact with error of {e}", + ) + return "" + + +def register_aicore_configuration( + aicore_artifact_id: str, + ai_core_client: AICoreV2Client, + resource_group: str, + accumulated_config_data: _EvaluationConfigData, + orchestration_url: str, + dataset_file_key: str, + run_ids_list: List[str], + llm_model_config: str, + template_config: List, + orchestration_registry_config: str, + error_collector: ValidationCollector, +): + try: + # logic to generate the uuids for templates or orchestration registry + dataset_type = accumulated_config_data.dataset_type + test_datasets = f'{{"path": "{dataset_file_key}", "type": "{dataset_type}"}}' + test_row_count = accumulated_config_data.test_row_count + distinct_metrics_list = list(set(accumulated_config_data.metrics_list)) + metrics_list = ",".join(distinct_metrics_list) + variable_mapping = json.dumps( + accumulated_config_data.variable_mapping + ) # to validate if this generates the right escaped string + + logger.info( + "The generated variable mapping is %s", + variable_mapping, + ) + tags = accumulated_config_data.tags + run_ids = ",".join( + run_ids_list + ) # underlying executable expects string as a parameter + + repetitions = accumulated_config_data.repetitions + random_id = generate_random_id()[:7] + configuration_name = EVALUATIONS_CONFIG_PREFIX_KEY + random_id + parameter_bindings_list = [ + ParameterBinding(key="repetitions", value=str(repetitions)), + ParameterBinding(key="orchestrationDeploymentURL", value=orchestration_url), + ParameterBinding(key="tags", value=str(tags)), + ParameterBinding(key="variableMapping", value=variable_mapping), + ParameterBinding(key="metrics", value=metrics_list), + ParameterBinding(key="testDataset", value=test_datasets), + ParameterBinding(key="testRowCount", value=str(test_row_count)), + ParameterBinding( + key="runIds", value=run_ids + ), # explicilty passing created run ids to the executable to map or create runs with the same id's + ] + if not accumulated_config_data.debug_mode: + parameter_bindings_list.append( + ParameterBinding(key="debugMode", value="OFF") + ) + + if llm_model_config is not None: + parameter_bindings_list.append( + ParameterBinding(key="models", value=llm_model_config) + ) + parameter_bindings_list.append( + ParameterBinding(key="promptTemplate", value=template_config[0]) + ) + else: + parameter_bindings_list.append( + ParameterBinding( + key="orchestrationRegistryIds", value=orchestration_registry_config + ) + ) + + response = ai_core_client.configuration.create( + name=configuration_name, + scenario_id=EVALUATIONS_SCENARIO_ID, + # executable_id=EVALUATIONS_SCENARIO_ID, + executable_id="genai-evaluations-simplified", # to replace with main simplified executable once changes are merged, till then testing with locally built executable + parameter_bindings=parameter_bindings_list, + input_artifact_bindings=[ + InputArtifactBinding( + key="datasetFolder", artifact_id=aicore_artifact_id + ) + ], + resource_group=resource_group, + ) + + configuration_id = response.id + logger.info("configuration id created is %s", configuration_id) + return configuration_id + except Exception as e: + error_collector.add_error( + ErrorCode.CONFIGURATION_CREATION_FAILURE, + f"Error occurred while attempting to create aicore configuration with error of {e}", + ) + return None + + +def register_aicore_execution( + ai_core_client: AICoreV2Client, + configuration_id: str, + resource_group: str, + error_collector: ValidationCollector, +): + try: + response = ai_core_client.execution.create(configuration_id, resource_group) + execution_id = response.id + logger.info("result of execution creation is %s", execution_id) + return execution_id + except Exception as e: + error_collector.add_error( + ErrorCode.EXECUTION_CREATION_FAILURE, + f"Error occurred while attempting to create an execution with error of {e}", + ) + return None + + +def list_available_llm_models(ai_core_client: AICoreV2Client, resource_group: str): + try: + return ai_core_client.model.query(resource_group).resources + except Exception as e: + raise RuntimeError( + f"Failed to list the available models with the error of {e}" + ) from e + + +def fetch_orchestration_config_from_registry( + orchestration_registry_reference: str, + ai_core_client: AICoreV2Client, + error_collector: ValidationCollector, +): + # using restClient as the prompt registry haven't added orchestration registry endpoint yet in the sdk + # curl -X GET "{{apiurl}}/v2/registry/v2/orchestrationConfigs?name=example-orchestration-config&scenario=customer-support&version=0.0.1" \ + try: + orch_registry_url_path = ( + f"{ORCHESTRATION_REGISTRY_ENDPOINT}/{orchestration_registry_reference}" + ) + response = ai_core_client.rest_client.get(path=orch_registry_url_path) + return response["spec"] + + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Orchestration config GET request from Orchestration regsitry failed with error of {e}", + ) + return None + +def resolve_metric_identifiers( + metrics: List[MetricConfig], + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> List[Dict]: + """ + Resolves metric identifiers to metric template metadata. + """ + metric_templates: List[Dict] = [] + + for metric in metrics: + metric_info = _resolve_single_metric( + metric, + ai_core_client, + resource_group, + error_collector, + ) + + if metric_info: + metric_templates.append(metric_info) + + return metric_templates + +def _resolve_single_metric( + metric: MetricConfig, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> Dict | None: + metric_reference = metric.reference + + if metric_reference.id is not None: + return _resolve_metric_by_id( + metric_reference.id, + ai_core_client, + resource_group, + error_collector, + ) + + if _has_scenario_name_version(metric_reference): + return _resolve_metric_by_metadata( + metric_reference, + ai_core_client, + resource_group, + error_collector, + ) + + if _is_system_defined_metric(metric_reference): + return _resolve_system_metric( + metric_reference.name, + ai_core_client, + resource_group, + error_collector, + ) + + error_collector.add_error( + ErrorCode.METRIC_CONFIG_ERROR, + ( + "Could not resolve metric config from Metric Management Service. " + "Please provide one of id or name or (scenario,name,version) " + f"combination for this metric reference of {metric.reference}" + ), + ) + return None + +def _has_scenario_name_version(metric_reference) -> bool: + return all( + [ + metric_reference.scenario, + metric_reference.name, + metric_reference.version, + ] + ) + +def _is_system_defined_metric(metric_reference) -> bool: + return ( + metric_reference.name is not None + and metric_reference.name in SYSTEM_DEFINED_METRIC_MAPPING.values() + ) + + +def _resolve_metric_by_id( + metric_id: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> Dict | None: + metric_info = get_custom_metric_by_id( + metric_id, + ai_core_client, + resource_group, + error_collector, + ) + + if not metric_info: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"Could not resolve metric with ID {metric_id}", + ) + return None + + return metric_info + +def _resolve_metric_by_metadata( + metric_reference, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> Dict | None: + metric_info = get_metric_version_history( + metric_reference.scenario, + metric_reference.name, + metric_reference.version, + ai_core_client, + resource_group, + error_collector, + ) + + if not metric_info: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"Could not resolve metric {metric_reference} — no version history found", + ) + return None + + return metric_info + +def _resolve_system_metric( + metric_name: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> Dict | None: + metric_info = get_metric_template_info_from_server( + metric_name, + ai_core_client, + resource_group, + error_collector, + ) + + if not metric_info: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"Could not resolve metric {metric_name} — not found in Metric Server", + ) + return None + + return metric_info + + + +def resolve_metric_names( + metric_configs_list: List[MetricConfig], error_collector: ValidationCollector +): + resolved_metrics_list = [] + + for metric_config in metric_configs_list: + metric_reference = metric_config.reference + if metric_reference.id is not None: + resolved_metrics_list.append(metric_reference.id) + elif all( + [metric_reference.scenario, metric_reference.name, metric_reference.version] + ): + resolved_metrics_list.append( + "/".join( + [ + metric_reference.scenario, + metric_reference.name, + metric_reference.version, + ] + ) + ) + elif metric_reference.name is not None: + resolved_metrics_list.append(metric_reference.name) + else: + error_collector.add_error( + ErrorCode.METRIC_CONFIG_ERROR, + f"Could not identify metric name. Please provide one of id or name or (scenario,name,version) combination for this metric config of {metric_reference}", + ) + + return resolved_metrics_list diff --git a/packages/gen/gen_ai_hub/evaluations/utils/config_data_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/config_data_utils.py new file mode 100644 index 0000000..db852fe --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/config_data_utils.py @@ -0,0 +1,203 @@ +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations._internal._models import _AWSObjectStoreData +from gen_ai_hub.evaluations.constants import ( + PROMPT_TEMPLATE_METADATA_FIELDS, + PROMPT_TEMPLATE_ID_KEY, + TEST_PROMPT_TEMPLATE_NAME, + TEST_PROMPT_TEMPLATE_VERSION, + EVALUATIONS_SCENARIO_ID, +) +from gen_ai_hub.evaluations.utils.aicore_utils import ( + resolve_artifact_path, + fetch_orchestration_config_from_registry, + generate_random_id, +) +from gen_ai_hub.evaluations.utils.file_utils import ( + load_config_file, +) +from gen_ai_hub.evaluations.utils.gen_utils import resolve_orchestration_config_v2 +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubProxyClient +from gen_ai_hub.prompt_registry.client import PromptTemplateClient +from gen_ai_hub.prompt_registry.models.prompt_template import ( + PromptTemplateSpec, + PromptTemplate, +) +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def _fetch_template_by_guid( + prompt_template_client: PromptTemplateClient, + prompt_template_id: str, + error_collector: ValidationCollector, +): + try: + response = prompt_template_client.get_prompt_template_by_id(prompt_template_id) + return response.spec.template + except Exception as e: + error_collector.add_error( + ErrorCode.PROMPT_TEMPLATE_GET_ERROR, + f"Error: {e} while fetching the prompt template from this uuid: {prompt_template_id} from the prompt registry.", + ) + return None + + +def _register_prompt_template( + template_data, + prompt_template_client: PromptTemplateClient, + error_collector: ValidationCollector, +): + try: + if not isinstance(template_data, PromptTemplateSpec): + # handling for the case when just a prompt template str is passed. + template_data = PromptTemplateSpec(template=template_data) + + random_id = generate_random_id()[:7] + prompt_template_name = TEST_PROMPT_TEMPLATE_NAME + random_id + + response = prompt_template_client.create_prompt_template( + name=prompt_template_name, + version=TEST_PROMPT_TEMPLATE_VERSION, + scenario=EVALUATIONS_SCENARIO_ID, + prompt_template_spec=template_data, + ) + return response.id + except Exception as e: + error_collector.add_error( + ErrorCode.REGISTER_PROMPT_TEMPLATE_ERROR, + f"Registering Prompt template to Prompt registry failed with this error of:{e}", + ) + return None + + +def _get_prompt_template_uuid_by_metadata( + prompt_template_client: PromptTemplateClient, + template_ref: TemplateRef, + error_collector: ValidationCollector, +): + """Fetch template using metadata parameters and return uuid""" + try: + # Convert TemplateRef to dict (using model_dump for Pydantic v2) + template_ref_dict = template_ref.model_dump() if hasattr(template_ref, 'model_dump') else template_ref.to_dict() + template_ref_data = template_ref_dict["template_ref"] + response = prompt_template_client.get_prompt_templates( + scenario=template_ref_data["scenario"], + name=template_ref_data["name"], + version=template_ref_data["version"], + ) + prompt_template_id = response.resources[0].id + return prompt_template_id + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Prompt template fetch failed for the provided matching of the metadata for the provided {template_ref}, failed with the error of {e}", + ) + + +def get_orch_config_data( + evaluation_config: EvaluationConfig, + ai_core_client: AICoreV2Client, + gen_ai_hub_proxy_client: GenAIHubProxyClient, + error_collector: ValidationCollector, +): + orchestration_config_data = [] + prompt_template_client = PromptTemplateClient(gen_ai_hub_proxy_client) + llm = evaluation_config.llm + template_info = evaluation_config.template + orchestration_registry_reference = ( + evaluation_config.orchestration_registry_reference + ) + + # check if one of the llm and template is provided, then it is invalid config + if (llm is None) != (template_info is None): + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Only llm or only template cannot be provided for a orchestration config, please provide both or a different way of EvaluationConfig", + ) + + if llm is not None and template_info is not None: + template_data = [] + prompt_template_uuid = "" + if isinstance(template_info, str): + # template just as str-> so convert to the required dict + template_data = [PromptTemplate(role="user", content=template_info)] + + # get the corresponding uuid by uploading this prompt to prompt registry. + prompt_template_uuid = _register_prompt_template( + template_data, prompt_template_client, error_collector + ) + + elif isinstance(template_info, PromptTemplateSpec): + # no need to convert as provided is already a template dict + template_data = template_info.template + # get the corresponding uuid by uploading this prompt to prompt registry. + prompt_template_uuid = _register_prompt_template( + template_info, prompt_template_client, error_collector + ) + + elif isinstance(template_info, TemplateRef): + # orchestration_v2 has nested structure: template_info.template_ref contains TemplateRefByID or TemplateRefByScenarioNameVersion + template_ref_inner = template_info.template_ref + + if hasattr(template_ref_inner, PROMPT_TEMPLATE_ID_KEY): + # to load data from uuid + prompt_template_uuid = template_ref_inner.id + template_data = _fetch_template_by_guid( + prompt_template_client, prompt_template_uuid, error_collector + ) + + elif all( + hasattr(template_ref_inner, key) for key in PROMPT_TEMPLATE_METADATA_FIELDS + ): + prompt_template_uuid = _get_prompt_template_uuid_by_metadata( + prompt_template_client, template_info, error_collector + ) + template_data = _fetch_template_by_guid( + prompt_template_client, prompt_template_uuid, error_collector + ) + + else: + error_collector.add_error( + ErrorCode.INVALID_TEMPLATE_REFERENCE_KEY, + "Invalid template reference format, either use uuid or scenario/name/version format", + ) + # converting all template formats given back to uuid to be used for simplified executable + evaluation_config.template = prompt_template_uuid + orchestration_config_data = resolve_orchestration_config_v2(template_data, llm) + elif orchestration_registry_reference is not None: + # To handle both uuid and scenario/name/version + orchestration_config_data = fetch_orchestration_config_from_registry( + orchestration_registry_reference, ai_core_client, error_collector + ) + logger.info( + "The orchestration config that is being generated from the provided config is %s", + orchestration_config_data, + ) + + return orchestration_config_data + + +def get_dataset_data( + dataset_config: Dataset, + ai_core_client: AICoreV2Client, + object_store_credentials: _AWSObjectStoreData, + resource_group: str, + error_collector: ValidationCollector, +): + if isinstance(dataset_config.source, ArtifactSource): + # needs to resolve loading via artifact + return resolve_artifact_path( + dataset_config.source, + ai_core_client, + object_store_credentials, + resource_group, + error_collector, + ) + return load_config_file(dataset_config.source, error_collector) diff --git a/packages/gen/gen_ai_hub/evaluations/utils/file_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/file_utils.py new file mode 100644 index 0000000..e11494c --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/file_utils.py @@ -0,0 +1,109 @@ +from pathlib import Path +from typing import Union, Dict, Any, List +import json +import uuid +import pandas as pd +from gen_ai_hub.evaluations.constants import ( + CSV_FILE_TYPE, + JSON_FILE_TYPE, + JSONL_FILE_TYPE, + SUPPORTED_FILE_TYPES, +) +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def load_config_file( + file_path: Union[str, Path], error_collector: ValidationCollector +) -> Union[Dict[str, Any], List[Dict[str, Any]], List]: + """Load config from local file path or Path object""" + # Convert to Path object for consistent handling + path = Path(file_path).expanduser().resolve() + logger.info("Loading the local file from the path: %s", path) + + # Validate file exists + if not path.exists(): + error_collector.add_error( + ErrorCode.INVALID_FILE_PATH_ERROR, f"Provide Config file not found: {path}" + ) + return [] + + if not path.is_file(): + error_collector.add_error( + ErrorCode.INVALID_FILE_PATH_ERROR, + f"Provided Path: {path} is not an actual file. Please provide a file and not a directory!", + ) + return [] + + # Determine file type by extension + suffix = path.suffix.lower().lstrip(".") + + if suffix not in SUPPORTED_FILE_TYPES: + error_collector.add_error( + ErrorCode.UNSUPPORTED_FILE_TYPE_ERROR, + f"File type provided is not supported for this file: {path}. Please provide one of {SUPPORTED_FILE_TYPES}", + ) + return [] + + try: + with open(path, "r", encoding="utf-8") as f: + if suffix in [JSON_FILE_TYPE]: + return json.load(f) + if suffix in [CSV_FILE_TYPE]: + return read_local_csv_file(path, error_collector) + if suffix in [JSONL_FILE_TYPE]: + return [json.loads(line) for line in f] + + except json.JSONDecodeError as e: + error_collector.add_error( + ErrorCode.INVALID_JSON_DECODING_ERROR, + f"Failed to parse config file {path}: {e}", + ) + return [] + except (OSError, UnicodeDecodeError) as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"File access error for config file {path}: {e}" + ) + return [] + except ValueError as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"Value error reading config file {path}: {e}" + ) + return [] + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"Reading config file {path} failed with: {e}" + ) + return [] + + +def read_local_csv_file(file_path: Path, error_collector: ValidationCollector) -> List[Dict[str, Any]]: + """reads csv and returns the data as df""" + try: + df = pd.read_csv( + file_path, + quoting=1, + escapechar="\\", + encoding="utf-8", + keep_default_na=False, + dtype=str, + ) + return df.to_dict("records") + except pd.errors.ParserError as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"CSV parsing error in file: {file_path}: {e}" + ) + return [] + except OSError as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"File access error for csv file: {file_path}: {e}" + ) + return [] + except Exception as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, f"Reading config file {file_path} failed with: {e}" + ) + return [] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/evaluations/utils/gen_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/gen_utils.py new file mode 100644 index 0000000..6acf57e --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/gen_utils.py @@ -0,0 +1,1234 @@ +import json +import re +import random +import pandas as pd +from collections import defaultdict +from typing import List, Any, Dict, Optional, Set, Tuple +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplate +from gen_ai_hub.evaluations.constants import ( + VARIABLE_MAPPING_DATA_PREFIX_KEY, + MODEL_NAME_KEY, + MODEL_VERSION_KEY, + MODEL_CONFIGURATION_KEY, + LATEST_MODEL_VERSION_KEY, + MODEL_FILTER_LIST_KEY, + MODEL_FILTER_LIST_TYPE_KEY, + TEMPLATE_KEY, + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, + INPUT_VARIABLE_REGEX_PATTERN, + VALIDATION_REGEX_PATTERN_FOR_INPUT_VARIABLES, + PREDEFINED_SYSTEM_VARIABLES_LIST, + AICORE_LLM_PROMPT_TEMPLATE_KEY, + AICORE_LLM_COMPLETION_KEY, + PROMPT_KEY, + ALL_METRICS_COLUMN_MAPPING_KEY, + COLUMN_MAPPING_DEFAULT_KEYS, + JSON_SCHEMA_MATCH_METRIC_ID, + JSON_SCHEMA_KEY, + LANGUAGE_MATCH_METRIC_ID, + LANGUAGE_KEY, + REFERENCE_KEY, + AZURE_CONTENT_SAFETY_KEY, + FILTERS_KEY, + LLAMA_GUARD_CONTENT_SAFETY_KEY, + TYPE_KEY, + MODEL_KEY, + MODULES_KEY, + PROMPT_TEMPLATING_KEY, + ORCHESTRATION_CONFIGURATION_V2, + CONFIG_KEY, + ORCHESTRATION_CONFIG_TEMPLATE_V2, + LLM_MODULE_V2_NAME_KEY, + LLM_MODULE_V2_PARAMETERS_KEY, + LLM_MODULE_V2_VERSION_KEY, + PROMPT_REGISTRY_ROLE_KEY, + PROMPT_REGISTRY_CONTENT_KEY, + SYSTEM_DEFINED_METRIC_MAPPING, + LLM_AS_A_JUDGE, + ID, + VARIABLES_KEY, + NAME_KEY, +) +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.utils.language_match_utils import LanguageMapper +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def update_variable_mapping( + variable_mapping: dict, prefix_key: str, variable_mapping_dict: dict +) -> dict: + for key, value in variable_mapping.items(): + key = prefix_key + key + value = VARIABLE_MAPPING_DATA_PREFIX_KEY + value + variable_mapping_dict[key] = value + + return variable_mapping_dict + + +def get_accumulated_config_data(evaluation_configs_data: List[_EvaluationConfigData]) -> _EvaluationConfigData: + # case when all dataset data and all metrics across all evaluation configs are the same. + accumulated_orchestration_config_data: List[dict] = [] + accumulated_metrics_list: List[str] = evaluation_configs_data[0].metrics_list + accumulated_variable_mapping: dict = {} + accumulated_dataset_data: Any = evaluation_configs_data[0].dataset_data + dataset_file_type: str = evaluation_configs_data[0].dataset_type + accumulated_metric_templates: List[dict] = evaluation_configs_data[ + 0 + ].metric_templates + try: + for evaluation_config_data in evaluation_configs_data: + # accumulating only orchestration config data and variable mapping + accumulated_orchestration_config_data.extend( # we already get the data as a list + evaluation_config_data.orch_config_data + ) + if evaluation_config_data.variable_mapping is not None: + accumulated_variable_mapping.update( + evaluation_config_data.variable_mapping + ) + + result = _EvaluationConfigData( + orch_config_data=accumulated_orchestration_config_data, + metrics_list=accumulated_metrics_list, + metric_templates=accumulated_metric_templates, + variable_mapping=accumulated_variable_mapping, + dataset_data=accumulated_dataset_data, + dataset_type=dataset_file_type, + ) + + return result + except Exception as e: + raise RuntimeError( + f"Failed to accumulate data across multiple evaluation configs provided with error of {e}" + ) from e + + +def set_model_details_from_run_configs(orch_config) -> Optional[Tuple[str, str]]: + """ + Sets the model name and version from the run data if available. + If not available, it returns None. + """ + try: + llm_module = orch_config[MODULES_KEY][PROMPT_TEMPLATING_KEY][MODEL_KEY] + llm_model = llm_module.get("name") + if not llm_model: + return None + llm_version = llm_module.get("version") or "latest" + return llm_model, llm_version + except (KeyError, TypeError): + return None + + +def create_model_versions_map_from_orch_configs( + orchestration_configs_data: List[dict], error_collector: ValidationCollector +) -> Optional[Dict[str, List[str]]]: + model_versions_map = defaultdict(list) + for orch_config in orchestration_configs_data: + model_details = set_model_details_from_run_configs(orch_config) + + if model_details: + llm_model, llm_version = model_details + model_versions_map[llm_model].append(llm_version) + + if not model_versions_map: + error_collector.add_error( + ErrorCode.EMPTY_FIELD_NAME_ERROR, "No models found in run data." + ) + return None + + return model_versions_map + + +def parse_model_filter_list(param, error_collector: ValidationCollector) -> List: + """Parse and return model filter list from a param.""" + try: + model_list = param.value + + if model_list == "null": + model_list = None + + if model_list is None: + return [] + + if isinstance(model_list, str): + model_list = json.loads(model_list) + + if not isinstance(model_list, list): + error_collector.add_error( + ErrorCode.GENERIC_ERROR, "modelFilterList must be a list" + ) + return [] + + return model_list + + except (json.JSONDecodeError, TypeError) as e: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + f"Failed to parse modelFilterList, Invalid modelFilterList format in parameterBindings with error of {e}", + ) + return [] + + +def build_model_versions_map(model_list) -> Dict[str, List[str]]: + """Builds a map of model names to their versions.""" + model_versions_map = defaultdict(list) + if not model_list: + return model_versions_map + for model_entry in model_list: + model_name = model_entry.get("modelName") + versions = model_entry.get("modelVersions", []) + if model_name: + model_versions_map[model_name].extend(versions) + return model_versions_map + + +def create_model_versions_map_from_configuration_param_bindings( + param_bindings, error_collector: ValidationCollector +) -> Tuple[Dict[str, List[str]], Optional[str]]: + model_versions_map = defaultdict[Any, list](list) + model_filter_type = None + + for param in param_bindings: + if param.key == MODEL_FILTER_LIST_KEY: + model_list = parse_model_filter_list(param, error_collector) + model_versions_map = build_model_versions_map(model_list) + elif param.key == MODEL_FILTER_LIST_TYPE_KEY: + model_filter_type = param.value + + return model_versions_map, model_filter_type + + +def create_model_versions_map_from_custom_metric_config(custom_metric_config_data) -> Dict[str, List[str]]: + model_versions_map = defaultdict(list) + if not custom_metric_config_data: + return model_versions_map + + for metric in custom_metric_config_data: + model_config = metric.get(MODEL_CONFIGURATION_KEY, {}) + model_name = model_config.get(MODEL_NAME_KEY) + model_version = model_config.get(MODEL_VERSION_KEY) or LATEST_MODEL_VERSION_KEY + + if model_name and model_version: + model_versions_map[model_name].append(model_version) + + return model_versions_map + + +def select_model_details_randomly( + orchestration_config_data: List[dict], + error_collector: ValidationCollector, +) -> Optional[Tuple[str, str]]: + """ + Selects at random, model name and version from the list of model names and versions provided by the users run data. + """ + model_versions_map = create_model_versions_map_from_orch_configs( + orchestration_config_data, error_collector + ) + + if not model_versions_map: + return None + + model_name = random.choice(list(model_versions_map)) + model_version = random.choice(model_versions_map[model_name]) + logger.info( + f"Randomly chosen model and version is: {model_name} & {model_version}", + ) + return model_name, model_version + + +def update_test_orch_config( + model_name, model_version, error_collector: ValidationCollector +) -> Optional[dict]: + try: + ORCHESTRATION_CONFIGURATION_V2[CONFIG_KEY][MODULES_KEY][PROMPT_TEMPLATING_KEY][ + MODEL_KEY + ].update( + { + "name": model_name, + "version": model_version, + } + ) + return ORCHESTRATION_CONFIGURATION_V2[CONFIG_KEY] + except Exception as e: + error_collector.add_error( + ErrorCode.ORCHESTRATION_URL_VALIDATION_ERROR, + f"Error updating model name and version in TEST_ORCH_CONFIG: Key Error: {e}", + ) + return None + + +def has_filter_key(orch_config: dict) -> bool: + """ + Check if the 'filtering' key is present in the orchestration config. + + :param orch_config: Orchestration configuration dictionary. + :type orch_config: dict + :return: True if filtering key is present, False otherwise. + :rtype: bool + """ + return "filtering" in orch_config.get(MODULES_KEY, {}) + + +def get_filter_config(orch_config: dict) -> dict: + """ + Extract the filtering configuration from the orchestration config. + + :param orch_config: Orchestration configuration dictionary. + :type orch_config: dict + :return: Filtering configuration dictionary, or empty dict if not present. + :rtype: dict + """ + return orch_config.get(MODULES_KEY, {}).get("filtering", {}) + + +def check_if_content_filter_provider_supported( + orch_config: dict, error_collector: ValidationCollector +) -> bool: + """ + Validates if all filters in the filtering module configuration are of supported types. + + :param orch_config: Orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: True if all filters are supported or no filtering is configured, False otherwise. + :rtype: bool + """ + if not has_filter_key(orch_config): + return True + filter_config = get_filter_config(orch_config) + + for section_name, section in filter_config.items(): + for filter_obj in section[FILTERS_KEY]: + if filter_obj.get(TYPE_KEY) not in { + AZURE_CONTENT_SAFETY_KEY, + LLAMA_GUARD_CONTENT_SAFETY_KEY, + }: + error_collector.add_error( + ErrorCode.UNSUPPORTED_FILTER_TYPE_ERROR, + f"In provided orch config of {orch_config}, found unsupported filter type '{filter_obj[TYPE_KEY]}' " + f"in section '{section_name}'. Only '{AZURE_CONTENT_SAFETY_KEY}' is supported.", + ) + return False + + return True + + +def remove_filter_metrics_if_provider_not_supported( + orchestration_config_data: List[dict], + metrics: List[str], + error_collector: ValidationCollector, +) -> None: + """Removes content filter-related metric IDs from the metrics list if the content filter + provider is not supported for any of the runs in orchestration_config_data.""" + is_content_filter_provider_supported = True + for orch_config in orchestration_config_data: + if is_content_filter_provider_supported: + is_content_filter_provider_supported = ( + check_if_content_filter_provider_supported(orch_config, error_collector) + ) + if not is_content_filter_provider_supported: + metrics[:] = [ + metric + for metric in metrics + if metric + not in { + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, + } + ] + + +def create_custom_metric_name( + custom_metric_config: dict, error_collector: ValidationCollector +) -> str: + """ + Creates a custom metric name based on the provided custom metric configuration. + + :param custom_metric_config: Dictionary containing metric configuration. + :param error_collector: ValidationCollector instance for collecting validation errors. + :return: A string representing the custom metric name. + :raises ValidationError: If required fields are missing or invalid. + """ + + # Extract required fields + metric_id = custom_metric_config.get("metricId") + scenario = custom_metric_config.get("scenario") + metric_name = custom_metric_config.get("metricName") + version = custom_metric_config.get("version") + + # Validate inputs + if not metric_id: + if not scenario: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + "Missing 'scenario' field in custom metric configuration.", + ) + if not metric_name: + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + "Missing 'metricName' field in custom metric configuration.", + ) + elif metric_id and (scenario or metric_name): + error_collector.add_error( + ErrorCode.GENERIC_ERROR, + "Both 'metricId' and 'scenario/metricName' cannot be provided at the same time in the custom metric configuration.", + ) + + # Generate custom metric name + if metric_id: + return metric_id.strip() + # Only proceed if scenario and metric_name are not None/empty (errors already added above) + if not scenario or not metric_name: + return "" # Return empty string if required fields are missing + custom_metric_name = f"{scenario.strip()}/{metric_name.strip()}" + if version: + custom_metric_name += f"/{version.strip()}" + return custom_metric_name + + +def validate_metric_name( + metric: str, all_supported_metrics: List, error_collector: ValidationCollector +) -> None: + """Validates if metrics name is not empty and the value actually exists in the list of supported metrics""" + if metric == "": + error_collector.add_error( + ErrorCode.EMPTY_METRIC_NAME_ERROR, + "Metric name cannot be empty. Please provide a valid metric name", + ) + if metric not in all_supported_metrics: + error_collector.add_error( + ErrorCode.UNSUPPORTED_METRIC_ERROR, + f"{metric} is neither a system supported metric nor provided in custom metric configuration", + ) + + +def count_user_prompts_from_template_list(template_list) -> int: + user_prompts = [] + for item in template_list: + if item["role"] == "user": + user_prompts.append(item) + return len(user_prompts) + + +def get_template_list_from_orch_config(orch_config) -> List: + return orch_config[MODULES_KEY][PROMPT_TEMPLATING_KEY][PROMPT_KEY][TEMPLATE_KEY] + + +def validate_prompts_in_templating_module( + orchestration_config_data: List[dict], + metric: str, + error_collector: ValidationCollector, +) -> None: + """Checks whether the templating config provided in the Orchestration Config has exactly one user prompt""" + for orch_config in orchestration_config_data: + prompt_list = get_template_list_from_orch_config(orch_config) + user_prompt_count = count_user_prompts_from_template_list(prompt_list) + if user_prompt_count < 1: + error_collector.add_error( + ErrorCode.MISSING_USER_PROMPT_ERROR.value, + f"Missing user prompt in template list. Please provide exactly one user prompt for " + f"{orch_config} to evaluate {metric} metric", + ) + elif user_prompt_count > 1: + error_collector.add_error( + ErrorCode.MORE_THAN_ONE_USER_PROMPT_PROVIDED_ERROR.value, + f"More than one user prompts provided in template list. Please provide exactly one user prompt for " + f"{orch_config} to evaluate {metric} metric", + ) + + +def get_custom_metric_ids_from_input( + custom_metric_config_data: List[dict], error_collector: ValidationCollector +) -> list[str]: + """ + Retrieve custom metric ids from the file data provided by the user. + + :param custom_metric_config_data: List of dictionaries containing custom metric definitions. + :type custom_metric_config_data: List[dict] + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: List of custom metric ids. + :rtype: list[str] + """ + custom_metrics_ids = [] + if not custom_metric_config_data: + return custom_metrics_ids + for custom_metric_config in custom_metric_config_data: + custom_metric_name = create_custom_metric_name( + custom_metric_config, error_collector + ) + custom_metrics_ids.append(custom_metric_name) + return custom_metrics_ids + + +def check_if_metric_is_defined( + metrics: List[str], + metric_templates: List[dict], + error_collector: ValidationCollector, +) -> None: + for metric in metrics: + if metric in SYSTEM_DEFINED_METRIC_MAPPING.values(): + continue + + found = False + for template in metric_templates: + if metric == template.get("id"): + found = True + break + + if "/" in metric and metric == template.get( + "scenario" + ) + "/" + template.get("name") + "/" + template.get("version"): + found = True + break + + if not found: + error_collector.add_error( + ErrorCode.EMPTY_METRIC_ERROR.value, + f"{metric} is neither a system supported metric nor provided in metric templates", + ) + + +def is_value_in_json(value, name, data: dict[str, str]) -> bool: + """ + Check if a value matches a name or exists in a mapping dictionary. + + :param value: The value to search for. + :type value: Any + :param name: The name to compare against. + :type name: str + :param data: Dictionary to search in (keys or values). + :type data: dict[str, str] + :return: True if value matches name or is found in data, False otherwise. + :rtype: bool + """ + value_str = str(value) + if value_str == name: + return True + return any(value_str == key or value_str == str(item) for key, item in data.items()) + + +def validate_metrics( + metrics: List[str], + metric_templates: List[dict], + orchestration_config_data: List[dict], + error_collector: ValidationCollector, +) -> None: + """Validates if metrics list is empty or metric name is invalid""" + if not metrics: + error_collector.add_error( + ErrorCode.EMPTY_METRIC_ERROR.value, + "Metrics list cannot be empty. Atleast one metric needs to be provided", + ) + + check_if_metric_is_defined(metrics, metric_templates, error_collector) + + for metric in metrics: + if metric == "": + error_collector.add_error( + ErrorCode.EMPTY_METRIC_ERROR.value, + "Metric name cannot be empty. Please provide a valid metric name", + ) + for template in metric_templates: + if ( + is_value_in_json(metric, template["id"], SYSTEM_DEFINED_METRIC_MAPPING) + and template.get("evaluationMethod", "") == LLM_AS_A_JUDGE + ): + validate_prompts_in_templating_module( + orchestration_config_data, metric, error_collector + ) + + +def _is_field_name_valid(field_name: str) -> bool: + """ + Validates the parameter field name that is specified in the prompt template variables. + """ + if not field_name: + raise ValueError( + ErrorCode.EMPTY_FIELD_NAME_ERROR, "Parameter names cannot be empty." + ) + # field_names must start with a character and end with a character or number + # only special characters allowed are _ and - and multiple consecutive - and _ are disallowed + validation_pattern = re.compile(VALIDATION_REGEX_PATTERN_FOR_INPUT_VARIABLES) + if not validation_pattern.match(field_name): + raise ValueError( + ErrorCode.GENERIC_ERROR, + "Parameter names in templates must be of the form {{ ?parameter_name }}. They must start with a character and end with a character or number. Only special characters allowed are _ and - . Multiple consecutive - and _ are disallowed.", + ) + return True + + +def list_prompt_variables(format_string: str) -> list[str]: + """ + Get all fields (parameters) of the form {{ ?param_name }} from the template. + Optionally return the raw field names without stripping spaces and '?'. + """ + field_names = re.findall(INPUT_VARIABLE_REGEX_PATTERN, format_string) + field_names = [ + (field_name.strip()[1:]) + for field_name in field_names + if field_name.strip().startswith("?") + ] + field_names = [ + (field_name) + for field_name in field_names + if _is_field_name_valid(field_name=field_name) + ] + return field_names + + +def extract_dataset_columns(template_variables) -> List[str]: + """Extracts column names from the template variables provided. If the value is a list, extracts column names from the first rows; else, extracts from the template_variables directly.""" + if isinstance(template_variables, list) and template_variables: + return list(template_variables[0].keys()) + elif isinstance(template_variables, dict): + return list(template_variables.keys()) + return [] + + +def get_prompt_variables_from_orch_config(orch_config: dict) -> Set[str]: + var_set = set() + template_list = get_template_list_from_orch_config(orch_config) + for template in template_list: + template_data = template["content"] + variables = [] + if isinstance(template_data, list): + for data in template_data: + variables.extend(list_prompt_variables(data["text"])) + elif isinstance(template_data, str): + variables = list_prompt_variables(template["content"]) + + var_set.update(variables) + # removes 'output_param' if it exists in the set + return _remove_output_param_if_present(var_set, orch_config) + + +def get_grounding_config_from_orch_config(orch_config) -> dict: + """ + Extracts the grounding configuration from the orchestration configuration. + Args: + orch_config (dict): Orchestration configuration. + Returns: + dict: Grounding configuration if present, otherwise an empty dictionary. + """ + return orch_config[MODULES_KEY].get("grounding", {}) + + +def get_grounding_output_param_key(orch_config: dict) -> str: + """ + Determines the correct key to extract the grounding output parameter based on the API version. + Args: + orch_config (dict): Orchestration configuration. + Returns: + str: The key to extract the grounding output parameter. + """ + return ( + orch_config.get(MODULES_KEY, {}) + .get("grounding", {}) + .get("placeholders", {}) + .get("output") + ) + + +def get_defaults(orchestration_configuration: dict) -> dict: + """Returns the default field from the orchestration configuration.""" + templating_config = orchestration_configuration.get(MODULES_KEY, {}).get( + PROMPT_TEMPLATING_KEY, {} + ) + return templating_config.get(PROMPT_KEY, {}).get("defaults", {}) + + +def _remove_output_param_if_present(var_set: set[str], orch_config: dict) -> set[str]: + """Removes the 'output_param' from variable set if present in the grounding config.""" + grounding_config = get_grounding_config_from_orch_config(orch_config) + if grounding_config: + output_param = get_grounding_output_param_key(orch_config) + if output_param and output_param in var_set: + var_set.remove(output_param) + return var_set + + +def get_mapped_value_if_exists(key, mapping_keys, variable_mapping, dataset_columns) -> str: + """Gets the first valid mapped value from the list of keys if it exists in variable mapping, else returns the first key""" + # Ensure mapping_keys is a list, even if it's a single string + if isinstance(mapping_keys, str): + mapping_keys = [mapping_keys] + for mapping_key in mapping_keys: + if mapping_key in variable_mapping: + key_value = variable_mapping[mapping_key] + prefix, field = key_value.split("/") + + # If mapped value exists in dataset, use it + if prefix == "data" and field in dataset_columns: + return field + + # If no mapping exists, return the key + return key + + +def validate_variable_mapping_of_prompts( + orchestration_config_data: list, + dataset_data: List[dict], + variable_mapping: dict, + error_collector: ValidationCollector, +) -> None: + """ + Validates the variable mapping for prompts with a zero-tolerance failure threshold. + + Args: + orchestration_config_data (list): Orchestration run configuration + dataset_data (dict): Dataset rows to validate + variable_mapping (dict): The variable mapping provided in the input configuration. + + Raises: + ValidationError: If any prompts variable mapping is invalid or does not exist in the dataset. + """ + + # Get list of variables from each orch config and validate + dataset_columns = extract_dataset_columns(dataset_data) + for orch_config in orchestration_config_data: + prompt_variables = get_prompt_variables_from_orch_config(orch_config) + # Extract default values from templating_module_config + defaults = get_defaults(orch_config) + # adding a validation check to see if system defined variables exits in the list of variables + if set(prompt_variables) & set(PREDEFINED_SYSTEM_VARIABLES_LIST): + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"System defined variables {AICORE_LLM_PROMPT_TEMPLATE_KEY} or {AICORE_LLM_COMPLETION_KEY} cannot be used as prompt variables inside the Run Configuration.", + ) + + for var in prompt_variables: + # Skip validation for variables that have default values + if defaults is not None and var in defaults: + continue + key_name = f"prompt/{var}" + key_value = get_mapped_value_if_exists( + var, key_name, variable_mapping, dataset_columns + ) + if key_value not in dataset_columns: + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"The provided prompt variable :{var} in Orch config does not match with any variable mapping provided or the actual dataset rows for this orch config of: {orch_config}", + ) + + +def validate_all_metrics_mapping( + variable_mapping: dict, dataset_columns: list, error_collector: ValidationCollector +) -> None: + """ + Validates the variable mapping for 'all_metrics' with a zero-tolerance failure threshold. + + Args: + variable_mapping (dict): The variable mapping provided in the input configuration. + dataset_columns (list): List of column names in the dataset. + + Raises: + ValidationError: If any 'all_metrics' mapping is invalid or the direct column does not exist in the dataset. + """ + all_metrics_keys = [ + key for key in variable_mapping.keys() if ALL_METRICS_COLUMN_MAPPING_KEY in key + ] + + for all_metric_key in all_metrics_keys: + all_metric_variable_value = variable_mapping[all_metric_key] + default_column_values = all_metric_key.split("/") + default_column_value = "/".join(default_column_values[:-1]) + dataset_column_name = all_metric_variable_value.split("/")[1] + + if ( + dataset_column_name not in dataset_columns + and default_column_value not in dataset_columns + ): + error_collector.add_error( + ErrorCode.GENERIC_ERROR.value, + f"The provided column mapping for all_metrics of {all_metric_key}:{all_metric_variable_value} " + f"in the variable mapping does not exist in the dataset provided nor the actual variable column.", + ) + + +def extract_metrics_variables(metric_templates, metric_name: str = None) -> Set: + """Extracts unique set of variables from the 'variables' key.""" + variables_list = set() # Use a set to store unique values + for current_metric in metric_templates: + if metric_name in (current_metric[ID], current_metric[NAME_KEY]): + return current_metric.get("additionalProperties", {}).get(VARIABLES_KEY, []) + variables_list.update( + current_metric.get("additionalProperties", {}).get(VARIABLES_KEY, []) + ) + + return variables_list + + +def validate_individual_metrics( + metrics: list[str], + variable_mapping: dict, + dataset_columns: list, + metric_dependent_variables: set, + error_collector: ValidationCollector, +) -> None: + """ + Validates the variable mapping for any metric mapping with a zero-tolerance failure threshold. + + Args: + metrics (list): List of metrics provided in the input configuration. + variable_mapping (dict): The variable mapping provided in the input configuration. + dataset_columns (list): List of column names in the dataset. + metric_dependent_variables (set): Set of dependent variables for all metrics + + Raises: + ValidationError: If any metric mapping is invalid or the direct column does not exist in the dataset. + """ + temp_metrics = metrics + + for key, value in variable_mapping.items(): + key_parts = key.split("/") + mapping_key = "/".join(key_parts[:-1]) + default_mapping_value = key_parts[-1] + _, dataset_value = value.split("/") + + # Skip if not a metric or not in the metrics list + if ( + mapping_key in COLUMN_MAPPING_DEFAULT_KEYS + or mapping_key not in temp_metrics + ): + continue + + if default_mapping_value not in metric_dependent_variables: + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"Invalid mapping value provided: {key}:{value} in the variable mapping. " + f"For system defined metrics, the list of dependent variables are {metric_dependent_variables}", + ) + + if ( + dataset_value not in dataset_columns + and default_mapping_value not in dataset_columns + ): + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"The provided mapping of {key}:{value} in the variable mapping is not valid as the dataset " + f"does not neither contains the column values provided in the mapping or the direct column name", + ) + + +def validate_variable_mapping_of_metrics( + metrics: list[str], + metric_templates: list[dict], + dataset_data: List[dict], + variable_mapping: dict, + error_collector: ValidationCollector, +) -> None: + """ + Validates variable mapping of metrics with tolerance to zero failure threshold + + Args: + metrics: List of metrics provided in the input config + metric_templates (list[dict]): Metric templates information resolved from Metric Management Service + dataset_data: Dataset rows to validate + variable_mapping: variable mapping provided in input config + + Returns: + Validates and throws validation error even if one variable mapping related to metrics is invalid. + """ + dataset_columns = extract_dataset_columns(dataset_data) + + # Validate all_metrics mappings + validate_all_metrics_mapping(variable_mapping, dataset_columns, error_collector) + # Validate individual metrics mappings + + metric_dependent_variables = extract_metrics_variables(metric_templates) + validate_individual_metrics( + metrics, + variable_mapping, + dataset_columns, + metric_dependent_variables, + error_collector, + ) + + +def flatten_prompt_configuration(prompt_config: dict) -> str: + """ + Flatten a nested prompt configuration dictionary into a readable string. + """ + + def format_value(val): + if isinstance(val, dict): + return ", ".join(f"{k}: {format_value(v)}" for k, v in val.items()) + elif isinstance(val, list): + return "; ".join(format_value(item) for item in val) + else: + return str(val) + + parts = [f"{k}: {format_value(v)}" for k, v in prompt_config.items()] + return "\n".join(parts) + + +def validate_individual_custom_metrics( + variable_mapping: dict, + dataset_columns: list, + custom_metric_ids: list, + custom_metric_variables: set, + error_collector: ValidationCollector, +) -> None: + """ + Validates the variable mapping for any metric mapping with a zero-tolerance failure threshold. + """ + custom_metric_variables -= set(PREDEFINED_SYSTEM_VARIABLES_LIST) + + _validate_empty_mapping_with_custom_vars( + variable_mapping, dataset_columns, custom_metric_variables, error_collector + ) + + for key, value in variable_mapping.items(): + _validate_mapping_entry( + key, + value, + custom_metric_ids, + custom_metric_variables, + dataset_columns, + error_collector, + ) + + +def _validate_empty_mapping_with_custom_vars( + variable_mapping: dict, + dataset_columns: List, + custom_metric_variables: set, + error_collector: ValidationCollector, +) -> None: + if not variable_mapping and custom_metric_variables: + unmapped_vars = { + var for var in custom_metric_variables if var not in dataset_columns + } + if unmapped_vars: + error_collector.add_error( + ErrorCode.GENERIC_ERROR.value, + "Variable mapping is empty, and the following custom metric variables are not found in the dataset: " + f"{unmapped_vars}. Either map them or ensure they are present in the dataset columns.", + ) + + +def _validate_mapping_entry( + key, + value, + custom_metric_ids, + custom_metric_variables, + dataset_columns, + error_collector: ValidationCollector, +) -> None: + key_parts = key.split("/") + if len(key_parts) < 3: + return + + mapping_key = "/".join(key_parts[:-1]) + mapping_value = key_parts[-1] + + try: + _, dataset_value = value.split("/") + except ValueError: + dataset_value = value + + if ( + mapping_key in COLUMN_MAPPING_DEFAULT_KEYS + or mapping_key not in custom_metric_ids + ): + return + + if ( + mapping_value not in custom_metric_variables + and mapping_value not in dataset_columns + ): + error_collector.add_error( + ErrorCode.GENERIC_ERROR.value, + f"Invalid mapping value provided: {key}:{value} in the variable mapping. " + f"For custom metrics, the list of dependent variables are {custom_metric_variables} " + f"and the available dataset columns are {dataset_columns}", + ) + + if dataset_value not in dataset_columns: + error_collector.add_error( + ErrorCode.GENERIC_ERROR.value, + f"The provided mapping of {key}:{value} in the variable mapping is not valid, as the dataset " + f"does not contain the column values provided in the mapping or the direct column name.", + ) + + +def handle_missing_dependent_variables_in_dataset( + dataset_data: List[dict], + metrics: list[str], + metric_templates: list[dict], + variable_mapping: dict, + error_collector: ValidationCollector, +) -> None: + """validates whether all the dependent variables for the metrics list are either directly present as columns in dataset or a variable mapping is provided + Args: + dataset_data (List[dict]): Dataset rows to validate (list of row dictionaries) + metrics (list[str]): List of metrics provided in the input configuration. + metric_templates (list[dict]): Metric templates information resolved from Metric Management Service + variable_mapping (dict): The variable mapping provided in the input configuration. + Raises: + ValidationError: If any dependent variable is missing in the dataset and the variable mapping is invalid for that metric. + """ + + dataset_columns = extract_dataset_columns(dataset_data) + for metric in metrics: + dependent_variables = extract_metrics_variables(metric_templates, metric) + # when no dependent variables are present for the metric - can be the case of custom metric or system defined metric with no dependent variables + if not dependent_variables: + continue + + for variable in dependent_variables: + # checking if variable mapping exists of some kind + first_mapping_key = f"{metric}/{variable}" + second_mapping_key = f"{ALL_METRICS_COLUMN_MAPPING_KEY}/{variable}" + if ( + first_mapping_key not in variable_mapping + and second_mapping_key not in variable_mapping + and variable not in dataset_columns + ): + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR, + f"Invalid mapping: The dependent variable '{variable}' for the metric '{metric}' is neither mapped correctly nor found as a direct column in the dataset.", + ) + + +def populate_dataset_data_if_data_missing( + dataset_data: list, variable_mapped_key, error_collector: ValidationCollector +) -> None: + """Validates and Populates the dataset_data with missing data fields if golden truth is present and throws error if dataset is partially filled""" + count_pre_filled_values_with_data = sum( + 1 + for row in dataset_data + if variable_mapped_key in row and row.get(variable_mapped_key) not in [None, ""] + ) + + if count_pre_filled_values_with_data == len(dataset_data): + # as populating is done across all rows we just return as populating is not required. + return + + if count_pre_filled_values_with_data != 1: + # raise an error saying only one or all can be provided it cannot be partial + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"Only one or all rows can be provided with '{variable_mapped_key}' in the dataset. Partial rows count is not allowed", + ) + # Find the value of the key in the first occurrence be it any row + first_data_value = next( + ( + row.get(variable_mapped_key) + for row in dataset_data + if variable_mapped_key in row + and row.get(variable_mapped_key) not in [None, ""] + ), + None, + ) + + if first_data_value is None: + error_collector.add_error( + ErrorCode.INVALID_METRIC_MAPPING_ERROR.value, + f"At least one valid data entry needs to be provided for '{variable_mapped_key}' in the dataset.", + ) + + # If the value exists, replicate it across all rows if any row does not have this column value + for row in dataset_data: + if ( + variable_mapped_key not in row + or pd.isna(row[variable_mapped_key]) + or row[variable_mapped_key] == "" + ): # if key is not present or value is None or null then populate + row[variable_mapped_key] = first_data_value + + +def populate_dataset_data_if_single_schema_provided( + dataset_data, variable_mapping, collector +) -> None: + """Populates the dataset_data with missing json schema column entries across rows of dataset_data""" + default_column_name = JSON_SCHEMA_KEY + mapping_key = f"{JSON_SCHEMA_MATCH_METRIC_ID}/{default_column_name}" + dataset_columns = extract_dataset_columns(dataset_data) + variable_mapped_key = get_mapped_value_if_exists( + default_column_name, mapping_key, variable_mapping, dataset_columns + ) + populate_dataset_data_if_data_missing(dataset_data, variable_mapped_key, collector) + + +def handle_json_schema_match( + metrics: list[str], + dataset_data: list, + variable_mapping: dict, + error_collector: ValidationCollector, +) -> None: + if JSON_SCHEMA_MATCH_METRIC_ID in metrics: + # populates all the rows of test data if golden instance is provided. + populate_dataset_data_if_single_schema_provided( + dataset_data, variable_mapping, error_collector + ) + logger.info( + "template vars data after modifying incase of missing rows and %s is %s ", + JSON_SCHEMA_MATCH_METRIC_ID, + dataset_data, + ) + + +def validate_language_code_and_data_population( + dataset_data: list, variable_mapping: dict, error_collector: ValidationCollector +) -> None: + """Populates the dataset_data with missing language column entries across rows of dataset_data""" + mapping_key = f"{LANGUAGE_MATCH_METRIC_ID}/{LANGUAGE_KEY}" + dataset_columns = extract_dataset_columns(dataset_data) + variable_mapped_key = get_mapped_value_if_exists( + LANGUAGE_KEY, mapping_key, variable_mapping, dataset_columns + ) + populate_dataset_data_if_data_missing( + dataset_data, variable_mapped_key, error_collector + ) + + for row in dataset_data: + if variable_mapped_key in row and row.get(variable_mapped_key) in (None, ""): + error_collector.add_error( + ErrorCode.INVALID_DATASET_DATA_ERROR.value, + f"All rows must be provided with '{variable_mapped_key}' in the dataset. Partial rows count is not allowed", + ) + + code = row.get(variable_mapped_key) + target_iso_code = LanguageMapper.get_iso_code_639_1(code) + if target_iso_code is None: + error_collector.add_error( + ErrorCode.UNSUPPORTED_LANGUAGE_MATCH_ERROR.value, + f"{code} is not supported by the language match metric", + ) + + +def handle_language_match( + metrics: list, + dataset_data: list, + variable_mapping: dict, + error_collector: ValidationCollector, +) -> None: + if LANGUAGE_MATCH_METRIC_ID in metrics: + validate_language_code_and_data_population( + dataset_data, variable_mapping, error_collector + ) + logger.info( + "template vars data after modifying in case of missing rows and %s is %s", + LANGUAGE_MATCH_METRIC_ID, + dataset_data, + ) + + +def populate_dataset_data_if_single_reference_provided( + dataset_data: List[dict], variable_mapping: dict, collector +) -> None: + """Populates the dataset_data with missing reference column entries across rows of + dataset_data for only all metrics case where a golden reference is present + Args: + dataset_data (List[dict]): Dataset rows to validate (list of row dictionaries) + variable_mapping (dict): The variable mapping provided in the input configuration. + """ + default_column_name = REFERENCE_KEY + mapping_key = f"{ALL_METRICS_COLUMN_MAPPING_KEY}/{default_column_name}" # as reference needs to be there across all predefined system defined metrics + dataset_columns = extract_dataset_columns(dataset_data) + if mapping_key in variable_mapping or default_column_name in dataset_columns: + # only populate when a mapping exists for all metrics reference or directly the reference column exists in the dataset + variable_mapped_key = get_mapped_value_if_exists( + default_column_name, mapping_key, variable_mapping, dataset_columns + ) + populate_dataset_data_if_data_missing( + dataset_data, variable_mapped_key, collector + ) + + +def populate_dataset_data_if_individual_metric_reference_provided( + dataset_data: List[dict], + variable_mapping: dict, + metrics: list, + error_collector: ValidationCollector, +) -> None: + """populates reference value across all rows of dataset_data if individual metric reference is provided + and is different than all metrics reference provided. This population happens if the provided reference is a golden instance + + Args: + dataset_data (List[dict]): Dataset rows to validate (list of row dictionaries) + variable_mapping (dict): The variable mapping provided in the input configuration. + metrics (list): List of metrics provided in the input configuration. + """ + dataset_columns = extract_dataset_columns(dataset_data) + for metric in metrics: + default_column_name = REFERENCE_KEY + mapping_key = f"{metric}/{REFERENCE_KEY}" + # if a valid mapping exists for the metric with reference, then populate the data for that column + if mapping_key in variable_mapping: + variable_mapped_key = get_mapped_value_if_exists( + default_column_name, mapping_key, variable_mapping, dataset_columns + ) + # populates if mapped key exists and that mapping column is a single entry + populate_dataset_data_if_data_missing( + dataset_data, variable_mapped_key, error_collector + ) + + +def handle_reference_missing_rows( + dataset_data: List[dict], + variable_mapping: dict, + metrics: list, + error_collector: ValidationCollector, +) -> None: + """validates whether the reference columns in the rows are missing in the dataset for all metrics and for each individual metrics + + Args: + dataset_data (List[dict]): Dataset rows to validate (list of row dictionaries) + variable_mapping (dict): The variable mapping provided in the input configuration. + metrics (list): List of metrics provided in the input configuration. + + Raises: + ValidationError: If any required variable mapping is invalid or the default column does not exist in the dataset. + """ + + populate_dataset_data_if_single_reference_provided( + dataset_data, variable_mapping, error_collector + ) + populate_dataset_data_if_individual_metric_reference_provided( + dataset_data, variable_mapping, metrics, error_collector + ) + + +def update_artifact_dict(artifact_reference: ArtifactSource, artifact_dict_count: dict) -> None: + artifact_instance = artifact_reference.artifact + if isinstance(artifact_instance, str): + artifact_dict_count[artifact_instance] = ( + artifact_dict_count.get(artifact_instance, 0) + 1 + ) + else: + artifact_dict_count[artifact_instance.id] = ( + artifact_dict_count.get(artifact_instance.id, 0) + 1 + ) + + +def resolve_orchestration_config_v2(template_data: List[PromptTemplate], llm: LLM) -> dict: + orchestration_config_data = ORCHESTRATION_CONFIG_TEMPLATE_V2 + template_list_data = [] + for data in template_data: + current_template_data = {} + current_template_data[PROMPT_REGISTRY_ROLE_KEY] = data.role + current_template_data[PROMPT_REGISTRY_CONTENT_KEY] = data.content + template_list_data.append(current_template_data) + orchestration_config_data[MODULES_KEY][PROMPT_TEMPLATING_KEY][PROMPT_KEY][ + TEMPLATE_KEY + ] = template_list_data + llm_module = {} + llm_module[LLM_MODULE_V2_NAME_KEY] = llm.name + llm_module[LLM_MODULE_V2_VERSION_KEY] = llm.version + # Support both old (parameters) and new (params) attribute names + llm_module[LLM_MODULE_V2_PARAMETERS_KEY] = getattr(llm, 'params', None) or getattr(llm, 'parameters', {}) + orchestration_config_data[MODULES_KEY][PROMPT_TEMPLATING_KEY][MODEL_KEY] = ( + llm_module + ) + + return orchestration_config_data diff --git a/packages/gen/gen_ai_hub/evaluations/utils/language_match_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/language_match_utils.py new file mode 100644 index 0000000..6b2cea9 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/language_match_utils.py @@ -0,0 +1,38 @@ +from typing import Optional +import langcodes + +class LanguageMapper: + """ + Uses the langcodes library to convert language strings to normalized + ISO 639-1 strings, handling regional variants like zh-CN. + """ + # exclude Bosnian and Malay + EXCLUDED_CODES = {'bs', 'ms'} + + @classmethod + def get_iso_code_639_1(cls, code: str) -> Optional[str]: + """ + Converts a language code string to its ISO 639-1 two-letter code. + + :param code: Language code string (e.g., 'zh-CN', 'en_US', 'en') + :type code: str + :return: Two-letter ISO 639-1 code (e.g., 'zh', 'en'), or None if the code is invalid, excluded, or empty + :rtype: Optional[str] + """ + if not code: + return None + + try: + # langcodes.get() parses 'zh-CN', 'en_US', etc. + lang = langcodes.get(code) + + # .language returns the two-letter ISO 639-1 code (e.g., 'zh') + iso_code = lang.language + + if iso_code in cls.EXCLUDED_CODES: + return None + return iso_code + + except langcodes.LanguageTagError: + # Returns None if the input is complete gibberish + return None diff --git a/packages/gen/gen_ai_hub/evaluations/utils/metric_client_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/metric_client_utils.py new file mode 100644 index 0000000..18dae2d --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/metric_client_utils.py @@ -0,0 +1,249 @@ +import requests +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.constants import ( + CONTENT_TYPE, + METRIC_SERVER_ENDPOINT, + EVALUATION_METRICS_ENDPOINT, +) +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode + +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def _get_custom_metric_details( + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> dict | None: + """ + Fetches custom metric details from the given GenAI metrics server endpoint via a GET request. + + Note: Not using rest_client.get() because it converts camelCase to snake_case, + but the Metric Management Service requires exact camelCase field names. + + :param ai_core_client: AI Core client instance for API access. + :type ai_core_client: AICoreV2Client + :param resource_group: The resource group name. + :type resource_group: str + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: Parsed JSON response as a dictionary, or None if request fails. + :rtype: dict | None + """ + try: + url_built = f"{ai_core_client.base_url}{METRIC_SERVER_ENDPOINT}" + token = ai_core_client.rest_client.get_token() + headers = { + "Content-Type": CONTENT_TYPE, + "Authorization": token, # already sends token in Bearer token format + "AI-Resource-Group": resource_group, + } + response = requests.get( + url=url_built, + headers=headers, + ) + response = response.json() + return response + + except Exception as e: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"GenAI metrics server GET request encountered an exception. Error: {e}", + ) + return None + + +def get_custom_metric_by_id( + metric_id: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> dict | None: + """ + Fetches a specific custom metric by its ID from the GenAI metrics server. + + Note: Not using rest_client.get() because it converts camelCase to snake_case, + but the Metric Management Service requires exact camelCase field names like + 'additionalProperties' (would become 'additional_properties' if using rest_client). + + :param metric_id: The unique ID of the metric to retrieve. + :type metric_id: str + :param ai_core_client: AI Core client instance for API access. + :type ai_core_client: AICoreV2Client + :param resource_group: The resource group name. + :type resource_group: str + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: Parsed JSON response as a dictionary, or None if not found. + :rtype: dict | None + """ + path = f"{METRIC_SERVER_ENDPOINT}/{metric_id}" + logger.debug( + f"Sending GET request to {path} for metric ID: {metric_id}", + ) + try: + # Not using rest_client of aicore as fields are getting snake cased by the rest client while all keys in schema of metric client uses camelCase like additionalProperties. if used via rest client its returning additional_properties whereas we need additionalProperties + url_built = f"{ai_core_client.base_url}{path}" + token = ai_core_client.rest_client.get_token() + headers = { + "Content-Type": CONTENT_TYPE, + "Authorization": token, # already sends token in Bearer token format + "AI-Resource-Group": resource_group, + } + # response = requests.post(completion_url, json=test_orch_config, headers=headers) + + response = requests.get( + url=url_built, + headers=headers, + ) + response = response.json() + return response + except Exception as e: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"GenAI metrics server GET request encountered an exception. Error: {e}", + ) + return None + + +def get_metric_template_info_from_server( + metric: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + """ + Retrieves metric template information from the server by metric name. + + :param metric: The name of the metric to retrieve. + :type metric: str + :param ai_core_client: AI Core client instance for API access. + :type ai_core_client: AICoreV2Client + :param resource_group: The resource group name. + :type resource_group: str + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: Metric information dictionary, or None if not found. + :rtype: dict | None + """ + all_metric = _get_custom_metric_details( + ai_core_client, resource_group, error_collector + ) + metric_id = None + for current_metric in all_metric.get("resources", []): + if current_metric["name"] == metric: + metric_id = current_metric["id"] + if metric_id: + metric_info = get_custom_metric_by_id( + metric_id, ai_core_client, resource_group, error_collector + ) + return metric_info + return None + + +def get_metric_version_history( + scenario: str, + metric_id: str, + version: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +) -> dict | None: + """ + Fetches the version history for a specific evaluation metric in a scenario. + + Note: Not using rest_client.get() because it converts camelCase to snake_case, + but the Metric Management Service requires exact camelCase field names. + + :param scenario: The name of the scenario. + :type scenario: str + :param metric_id: The unique ID of the evaluation metric. + :type metric_id: str + :param version: The version of the metric. + :type version: str + :param ai_core_client: AI Core client instance for API access. + :type ai_core_client: AICoreV2Client + :param resource_group: The resource group name. + :type resource_group: str + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: Parsed JSON response as a dictionary, or None if not found. + :rtype: dict | None + """ + path = f"/lm/scenarios/{scenario}{EVALUATION_METRICS_ENDPOINT}/{metric_id}/versions/{version}/history" + logger.debug( + f"Sending GET request to {path} for scenario: {scenario}, metric: {metric_id}, version: {version}", + ) + + try: + # removed other valiations of Http status code being not found and other checks from the prompt eval repo as here it is being handled from the rest_client and not a seperate metric_management_service client + + url_built = f"{ai_core_client.base_url}{path}" + token = ai_core_client.rest_client.get_token() + headers = { + "Content-Type": CONTENT_TYPE, + "Authorization": token, # already sends token in Bearer token format + "AI-Resource-Group": resource_group, + } + response = requests.get( + url=url_built, + headers=headers, + ) + response = response.json() + resources = response.get("resources", []) + if resources: + return resources[0] # return the latest metric version + else: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"No version history resources found for scenario: {scenario}, metric: {metric_id}, version: {version}", + ) + return None + + except Exception as e: + error_collector.add_error( + ErrorCode.METRIC_SERVER_RESOLVE_ERROR, + f"GenAI metrics server GET request encountered an exception for metric version history. Scenario: {scenario}, metric: {metric_id}, version: {version}. Error: {e}", + ) + return None + + +def fetch_all_system_predefined_metrics( + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, +): + """ + Fetches all system-predefined metrics from the GenAI metrics server. + + :param ai_core_client: AI Core client instance for API access. + :type ai_core_client: AICoreV2Client + :param resource_group: The resource group name. + :type resource_group: str + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: List of system-predefined metric templates. + :rtype: list + :raises RuntimeError: If fetching system predefined metrics fails. + """ + try: + all_metrics_info = _get_custom_metric_details( + ai_core_client, resource_group, error_collector + ) + all_metric_templates = all_metrics_info.get("resources", []) + + predefined_metric_templates = [ + item + for item in all_metric_templates + if item.get("systemPredefined") + is True # filtering metric templates based on systemPredefined flag from metric management service + ] + + return predefined_metric_templates + except Exception as e: + raise RuntimeError( + f"System Predefined metrics obtained from Metric Management service failed with error of {e}" + ) diff --git a/packages/gen/gen_ai_hub/evaluations/utils/orch_config_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/orch_config_utils.py new file mode 100644 index 0000000..e5121bf --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/orch_config_utils.py @@ -0,0 +1,573 @@ +from typing import List, Any +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.constants import ( + TEMPLATE_REF_KEY, + TEMPLATE_KEY, + IMAGE_URL_KEY, + CONTENT_KEY, + ROLE_KEY, + TYPE_KEY, + MODULES_KEY, + PROMPT_TEMPLATING_KEY, + PROMPT_KEY, + MODEL_KEY, +) +from gen_ai_hub.evaluations.utils.gen_utils import ( + list_prompt_variables, +) +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig + + +def validate_mandatory_modules( + orch_config: dict, + error_collector: ValidationCollector, + module_key: str, + required_keys: list[str], + config_keys: list[str], +) -> None: + """ + Validates the presence of mandatory modules and their structure. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :param module_key: The key of the module to validate. + :type module_key: str + :param required_keys: List of required keys in the module. + :type required_keys: list[str] + :param config_keys: List of configuration keys to validate as dictionaries. + :type config_keys: list[str] + :return: None + :rtype: None + """ + if module_key not in orch_config: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"{module_key} is mandatory in the orchestration config of {orch_config}", + ) + return + + # Check if required keys exist in the module + module_config = orch_config[module_key] + temp_config = module_config + for key in required_keys: + if key not in module_config: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"{', '.join(required_keys)} is mandatory in the {module_key} field of the orchestration config of {orch_config}", + ) + return + temp_config = temp_config[key] + + # Validate if the required configurations are dictionaries + for key in config_keys: + try: + config = temp_config[key] + except KeyError: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"Missing inside here configuration for {key} in the orchestration config {temp_config}", + ) + continue + if not isinstance(config, dict): + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"{key} should be a valid dictionary in the provided orchestration config of {orch_config}", + ) + + +def validate_orch_config_mandatory_modules( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if the outer structure of the orchestration config is valid and exists. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + validate_mandatory_modules( + orch_config, + error_collector, + module_key=MODULES_KEY, + required_keys=[PROMPT_TEMPLATING_KEY], + config_keys=[PROMPT_KEY, MODEL_KEY], + ) + + +def get_model_name( + orch_config: dict, keys: list[str], error_collector: ValidationCollector +) -> Any: + """ + Retrieves the value from orch_config using the list of keys provided. + If any key in the path is missing, logs an error and returns None. + + Args: + orch_config (dict): The orchestration configuration JSON object. + keys (list): List of keys representing the path to the desired field. + error_collector (ValidationCollector): The error collector to log errors. + + Returns: + The value at the specified path in orch_config, or None if any key is missing. + """ + try: + for key in keys: + orch_config = orch_config[key] + return orch_config + except KeyError: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"Missing configuration for {' -> '.join(keys)} in the orchestration config of {orch_config}", + ) + return None + + +def validate_model_name( + orch_config: dict, keys: list[str], error_collector: ValidationCollector +) -> None: + """ + Validates if llm_module_config is a dict and the model name exists. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param keys: List of keys representing the path to the model name. + :type keys: list[str] + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if error_collector.has_error_code( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value + ): + # Skip further validation for this run + return + get_model_name(orch_config, keys, error_collector) + + +def validate_model_name_in_llm_module_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if the model configuration in llm_module_config is a dict and the model name exists. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + validate_model_name( + orch_config, + [MODULES_KEY, PROMPT_TEMPLATING_KEY, MODEL_KEY, "name"], + error_collector, + ) + + +def get_prompt_templating_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Returns the prompt_templating configuration from the orchestration config. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + prompt_templating_config = orch_config.get(MODULES_KEY, {}).get( + PROMPT_TEMPLATING_KEY, {} + ) + if TEMPLATE_REF_KEY in prompt_templating_config[PROMPT_KEY]: + error_collector.add_error( + ErrorCode.INVALID_TEMPLATE_MODULE_CONFIG_ERROR.value, + f"template_ref inside prompt is not yet supported in the genai-evaluation service in the orchestration config of {orch_config}", + ) + + +def validate_template_ref_absent_in_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if template_ref is given in templating module config and raises an error. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if error_collector.has_error_code( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value + ): + # Skip further validation for this run + return + get_prompt_templating_config(orch_config, error_collector) + + +def get_template_key( + orch_config: dict, keys: list[str], error_collector: ValidationCollector +) -> Any: + """ + Retrieves the value from orch_config using the list of keys provided. + + If any key in the path is missing, logs an error and returns None. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param keys: List of keys representing the path to the desired field. + :type keys: list[str] + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: The value at the specified path in orch_config, or None if any key is missing. + :rtype: Any | None + """ + try: + for key in keys: + orch_config = orch_config[key] + return orch_config + except KeyError: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value, + f"Missing 'template' key in the orchestration config of {orch_config}", + ) + return None + + +def validate_if_template_list_is_empty( + orch_config: dict, keys: list[str], error_collector: ValidationCollector +) -> None: + """ + Validates if template list exists and is not empty. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param keys: List of keys representing the path to the template list. + :type keys: list[str] + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if error_collector.has_error_code( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value + ) or error_collector.has_error_code( + ErrorCode.INVALID_TEMPLATE_MODULE_CONFIG_ERROR.value + ): + # Skip further validation for this run + return + template_list = get_template_key(orch_config, keys, error_collector) + + if not isinstance(template_list, list) or len(template_list) == 0: + error_collector.add_error( + ErrorCode.EMPTY_TEMPLATE_LIST_ERROR.value, + f"template list cannot be empty in prompt field inside the orchestration config of {orch_config}", + ) + + +def validate_if_template_list_is_empty_in_templating_module_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if template list exists and is not empty in the templating module config. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + validate_if_template_list_is_empty( + orch_config, + [MODULES_KEY, PROMPT_TEMPLATING_KEY, PROMPT_KEY, TEMPLATE_KEY], + error_collector, + ) + + +def get_template_list_from_orch_config(orch_config: dict) -> list: + """ + Returns the template list from the orchestration config. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :return: The template list from the orchestration config. + :rtype: list + """ + return ( + orch_config.get(MODULES_KEY, {}) + .get(PROMPT_TEMPLATING_KEY, {}) + .get(PROMPT_KEY, {}) + .get(TEMPLATE_KEY, []) + ) + + +def validate_if_content_inside_template_is_empty_in_templating_module_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if the template list is an array and is a valid dict and content exists in template. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if error_collector.has_error_code( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value + ) or error_collector.has_error_code( + ErrorCode.INVALID_TEMPLATE_MODULE_CONFIG_ERROR.value + ): + return + + template_list = get_template_list_from_orch_config(orch_config) + for template in template_list: + if ( + not isinstance(template, dict) + or CONTENT_KEY not in template + or ROLE_KEY not in template + ): + error_collector.add_error( + ErrorCode.EMPTY_TEMPLATE_LIST_ERROR.value, + f"Each template must be a dictionary containing 'content' and 'role' in {orch_config}", + ) + + +def validate_if_image_url_is_provided_in_content_type_inside_templating_module_config( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if inside the template list of templating_module_config if it has image_url type inside the content. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if error_collector.has_error_code( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR.value + ) or error_collector.has_error_code( + ErrorCode.INVALID_TEMPLATE_MODULE_CONFIG_ERROR.value + ): + return + + template_list = get_template_list_from_orch_config(orch_config) + + for template in template_list: + content = template.get(CONTENT_KEY) + if isinstance(content, list): # Only apply image_url check if content is list + for item in content: + if isinstance(item, dict) and item.get(TYPE_KEY) == IMAGE_URL_KEY: + error_collector.add_error( + ErrorCode.EMPTY_TEMPLATE_LIST_URL_ERROR.value, + f"image_url is not supported in the content of template in the orchestration config of {orch_config}", + ) + + +def _validate_grounding_output_param_in_prompt_variables( + orch_config: dict, + error_collector: ValidationCollector, + module_key: str, + grounding_key: str, + output_param_path: list[str], + template_path: list[str], + content_key: str, +): + # Check if module_key and grounding_key exist + if module_key not in orch_config: + return + module_section = orch_config[module_key] + if grounding_key not in module_section: + return + + # Get grounding config and output_param + grounding_config = module_section[grounding_key] + output_param = grounding_config + for key in output_param_path: + output_param = output_param.get(key, {}) + # If the last key doesn't exist, output_param will be {}, so check for str + if not isinstance(output_param, str) or not output_param: + return + + # Get template list + template_section = module_section + for key in template_path: + template_section = template_section.get(key, {}) + template_list = template_section if isinstance(template_section, list) else [] + + # Collect all variables from templates + all_variables = set() + for template in template_list: + content = template.get(content_key) + if isinstance(content, str): + variables = list_prompt_variables(content) + all_variables.update(variables) + + # Validate if output_param exists in prompt variables + if output_param not in all_variables: + error_collector.add_error( + ErrorCode.INVALID_GROUNDING_CONFIGURATION.value, + f"Grounding response '{output_param}' is not being used in the template in orch config of: {orch_config}", + ) + + +def validate_if_grounding_output_present_in_prompt_variables( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if the grounding output parameter is present in prompt variables. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + module_key = MODULES_KEY + grounding_key = "grounding" + output_param_path = ["config", "placeholders", "output"] + template_path = [PROMPT_TEMPLATING_KEY, PROMPT_KEY, TEMPLATE_KEY] + content_key = "content" + + _validate_grounding_output_param_in_prompt_variables( + orch_config, + error_collector, + module_key, + grounding_key, + output_param_path, + template_path, + content_key, + ) + + +def validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config: dict, error_collector: ValidationCollector +) -> None: + """ + Validates if all the input params of the grounding module exist in the prompt variables for v2 configuration. + + :param orch_config: The orchestration configuration dictionary. + :type orch_config: dict + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + if MODULES_KEY not in orch_config: + return + + if "grounding" in orch_config[MODULES_KEY]: + grounding_config = orch_config[MODULES_KEY]["grounding"] + input_params_list = ( + grounding_config.get("config", {}).get("placeholders", {}).get("input") + ) + template_list = orch_config[MODULES_KEY][PROMPT_TEMPLATING_KEY][PROMPT_KEY].get( + TEMPLATE_KEY, [] + ) + + if input_params_list: + all_variables = _get_variables_from_template_list(template_list) + _validate_if_input_param_exists_in_prompt_variables( + all_variables, error_collector, input_params_list, orch_config + ) + + +def _get_variables_from_template_list(template_list): + all_variables = set() + for template in template_list: + content = template[CONTENT_KEY] + if isinstance(content, str): + variables = list_prompt_variables(content) + all_variables.update(variables) + return all_variables + + +def _validate_if_input_param_exists_in_prompt_variables( + all_variables, + error_collector: ValidationCollector, + input_params_list: list[str], + orch_config: OrchestrationConfig, +): + # Validate if each of the input_param exists in prompt variables + for input_param in input_params_list: + # Validate if input_param exists in prompt variables + if input_param not in all_variables: + error_collector.add_error( + ErrorCode.INVALID_GROUNDING_CONFIGURATION.value, + f"Grounding input '{input_param}' is not being used in the template in run: {orch_config}", + ) + + +def validate_orchestration_params_from_evaluation_config( + evaluation_configs: List[EvaluationConfig], error_collector: ValidationCollector +) -> None: + """ + Validates orchestration parameters from evaluation configuration. + + Ensures that either orchestration_registry_reference is provided alone, + or both template and llm are provided together. + + :param evaluation_configs: List of evaluation configuration objects. + :type evaluation_configs: List[EvaluationConfig] + :param error_collector: ValidationCollector instance for collecting validation errors. + :type error_collector: ValidationCollector + :return: None + :rtype: None + """ + template_configs_list = [] + for current_evaluation_config in evaluation_configs: + template_configs_list.append(current_evaluation_config.template) + has_prompt = current_evaluation_config.template is not None + has_models = current_evaluation_config.llm is not None + has_orch_registry = ( + current_evaluation_config.orchestration_registry_reference is not None + ) + if has_orch_registry: + if has_prompt or has_models: + error_collector.add_error( + ErrorCode.INVALID_PARAMETER_VALUE_ERROR.value, + "When providing orchestration_registry_uuids, do not provide prompt_template or models", + ) + continue # to cover for all other configs before returning the error + + if not (has_prompt and has_models): + error_collector.add_error( + ErrorCode.INVALID_PARAMETER_VALUE_ERROR.value, + "When orchestration_registry_uuids is absent, both prompt_template and models are required.", + ) + + +def to_comparable(x) -> dict | Any: + """ + Converts an object to a comparable format (dict or primitive). + + Handles Pydantic v1, Pydantic v2, custom classes, and primitives. + + :param x: The object to convert. + :type x: Any + :return: Dictionary representation or the primitive value. + :rtype: dict | Any + """ + if hasattr(x, "model_dump"): # Pydantic v2 + return x.model_dump() + if hasattr(x, "dict"): # Pydantic v1 + return x.dict() + if hasattr(x, "__dict__"): # Custom classes like TemplateRef + return vars(x) + return x # primitives like string + diff --git a/packages/gen/gen_ai_hub/evaluations/utils/oss_secret_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/oss_secret_utils.py new file mode 100644 index 0000000..cf0c9d0 --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/oss_secret_utils.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.constants import ( + DEFAULT_KEY, + AWS_S3_OSS_TYPE_KEY, + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, +) +from ai_api_client_sdk.exception import AIAPINotFoundException + +@dataclass +class ObjectStoreData: + provider_name: str + aws_access_key_id: str + aws_secret_access_key: str + + +def create_aws_object_store_secret( + aws_access_key_id: str, + aws_secret_access_key: str, + ai_core_client: AICoreV2Client, + resource_group: str, + secret_body: dict, + is_default_secret: bool, +): + """creates the s3 based object store secrets in aicore environment""" + try: + secret_data = secret_body.get("data", {}) or {} + + aws_access_key_id = secret_data.get(AWS_ACCESS_KEY_ID, aws_access_key_id) + aws_secret_access_key = secret_data.get( + AWS_SECRET_ACCESS_KEY, aws_secret_access_key + ) + secret_name = DEFAULT_KEY if is_default_secret else secret_body.get("name") + if not secret_name: + raise KeyError( + "Error while creating the object store secret. Name is mandatory to create the secret" + ) + response = ai_core_client.object_store_secrets.create( + name=secret_name, + type=AWS_S3_OSS_TYPE_KEY, + data={ + # use if overridden from user + AWS_ACCESS_KEY_ID: aws_access_key_id, + AWS_SECRET_ACCESS_KEY: aws_secret_access_key, + }, + bucket=secret_body.get("bucket"), + endpoint=secret_body.get("endpoint"), + region=secret_body.get("region"), + path_prefix=secret_body.get("pathPrefix", ""), + verifyssl=secret_body.get("verifyssl", ""), + usehttps=secret_body.get("usehttps", ""), + resource_group=resource_group, + ) + return response + + except Exception as e: + raise ValueError( + f"Error while creating the Object Store secret. Request failed with error of {e}" + ) from e + + +def fetch_object_store_secret_by_name( + ai_core_client: AICoreV2Client, + name: str, + resource_group: str, + collector: ValidationCollector, +): + try: + response = ai_core_client.object_store_secrets.get(name, resource_group) + return response + except Exception as e: + collector.add_error( + ErrorCode.GET_OBJECT_STORE_SECRET_ERROR.value, + f"Fetching Object Store secret failed with error of {e}", + ) + return None + + +def delete_object_store_secret( + ai_core_client: AICoreV2Client, name: str, resource_group: str +): + """Delete an object store secret. Returns None if secret doesn't exist (404 error).""" + + try: + response = ai_core_client.object_store_secrets.delete(name, resource_group) + return response + except AIAPINotFoundException: + # Secret doesn't exist, which is fine when trying to delete + return None + except Exception as e: + raise KeyError( + f"Error while deleting the Object Store secret. Request failed with error of {e}" + ) from e diff --git a/packages/gen/gen_ai_hub/evaluations/utils/validation_utils.py b/packages/gen/gen_ai_hub/evaluations/utils/validation_utils.py new file mode 100644 index 0000000..bd48cdd --- /dev/null +++ b/packages/gen/gen_ai_hub/evaluations/utils/validation_utils.py @@ -0,0 +1,343 @@ +from typing import List + +from ai_api_client_sdk.models.status import Status +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData +from gen_ai_hub.evaluations.utils.aicore_utils import ( + fetch_deployment_config, + fetch_configuration_by_id, + call_orchestration_service_with_v2_config, +) +from gen_ai_hub.evaluations.utils.gen_utils import ( + create_model_versions_map_from_orch_configs, + create_model_versions_map_from_configuration_param_bindings, + select_model_details_randomly, + validate_metrics, + remove_filter_metrics_if_provider_not_supported, + validate_variable_mapping_of_prompts, + validate_variable_mapping_of_metrics, + handle_missing_dependent_variables_in_dataset, + handle_json_schema_match, + handle_language_match, + handle_reference_missing_rows, + update_test_orch_config, +) + +from gen_ai_hub.evaluations.utils.orch_config_utils import ( + validate_orch_config_mandatory_modules, + validate_model_name_in_llm_module_config, + validate_template_ref_absent_in_config, + validate_if_template_list_is_empty_in_templating_module_config, + validate_if_content_inside_template_is_empty_in_templating_module_config, + validate_if_image_url_is_provided_in_content_type_inside_templating_module_config, + validate_if_grounding_output_present_in_prompt_variables, + validate_if_all_grounding_input_params_present_in_prompt_variables, +) + + +from gen_ai_hub.evaluations.helpers.logging import get_logger + +logger = get_logger() + + +def validate_filtered_models( + configuration_param_bindings, + orchestration_config_data: List[dict], + error_collector: ValidationCollector, +): + run_config_model_versions_map = create_model_versions_map_from_orch_configs( + orchestration_config_data, error_collector + ) + orch_config_model_versions_map, model_filter_type = ( + create_model_versions_map_from_configuration_param_bindings( + configuration_param_bindings, error_collector + ) + ) + + if not orch_config_model_versions_map: + logger.info( + "No model filter list provided — skipping model filtering.", + ) + return + + if model_filter_type not in {"allow", "deny"}: + error_collector.add_error( + ErrorCode.INVALID_FILTER_TYPE_ERROR, + f"Unsupported model filter type: {model_filter_type}. Expected 'allow' or 'deny'.", + ) + return + + if model_filter_type == "allow": + _validate_allowed_models( + run_config_model_versions_map, + orch_config_model_versions_map, + error_collector, + ) + else: # deny + _validate_denied_models( + run_config_model_versions_map, + orch_config_model_versions_map, + error_collector, + ) + + +def _validate_allowed_models( + run_models_map, allowed_models_map, error_collector: ValidationCollector +): + for model_name, versions in run_models_map.items(): + for version in versions: + if not _is_model_version_allowed(model_name, version, allowed_models_map): + error_collector.add_error( + ErrorCode.MODEL_NOT_ALLOWED_ERROR.value, + f"Model '{model_name}' with version '{version}' from run/custom metric configuration, is not in the allowlist.", + ) + + +def _validate_denied_models( + run_models_map, denied_models_map, error_collector: ValidationCollector +): + for model_name, versions in run_models_map.items(): + for version in versions: + if _is_model_version_denied(model_name, version, denied_models_map): + error_collector.add_error( + ErrorCode.MODEL_NOT_ALLOWED_ERROR.value, + f"Model '{model_name}' with version '{version}' from run/custom metric configuration, is explicitly denied.", + ) + + +def _is_model_version_allowed(model_name, version, allowed_models_map): + return ( + model_name in allowed_models_map and version in allowed_models_map[model_name] + ) + + +def _is_model_version_denied(model_name, version, denied_models_map): + return model_name in denied_models_map and version in denied_models_map[model_name] + + +def fetch_and_validate_orchestration_config( + ai_core_client: AICoreV2Client, + configuration_id: str, + orchestration_config_data: List[dict], + resource_group: str, + error_collector: ValidationCollector, +): + configuration_response = fetch_configuration_by_id( + configuration_id, ai_core_client, resource_group, error_collector + ) + configuration_param_bindings = configuration_response.parameter_bindings + validate_filtered_models( + configuration_param_bindings, + orchestration_config_data, + error_collector, + ) + + +def validate_orchestration_url_across_configs( + accumulated_config_data: List[_EvaluationConfigData] | _EvaluationConfigData, + orchestration_url: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, + proxy_client=None, +): + """wrapper function to perform validation of fetched config data in case of single vs multiple executions flow""" + items = ( + accumulated_config_data + if isinstance(accumulated_config_data, list) + else [accumulated_config_data] + ) + + for current_config_data in items: + validate_orchestration_url( + current_config_data, + orchestration_url, + ai_core_client, + resource_group, + error_collector, + proxy_client, + ) + +def extract_deployment_id(orch_url) -> str: + return orch_url.rstrip("/").split("/")[-1] + +def validate_orchestration_url( + evaluation_config_data: _EvaluationConfigData, + orchestration_url: str, + ai_core_client: AICoreV2Client, + resource_group: str, + error_collector: ValidationCollector, + proxy_client=None, +): + """ + Validates if the orchestration deployment url provided via config resides in same + resourceGroup as workload or not. Also validates if url is valid + and orchestration deployment is not in terminal state + """ + logger.info( + "Validating the user provided Orchestration Deployment URL with a test orch config" + ) + + deployment_id = extract_deployment_id(orchestration_url) + + deployment_config = fetch_deployment_config( + deployment_id, ai_core_client, resource_group, error_collector + ) + + deployment_status = deployment_config.status + config_id = deployment_config.configuration_id + orch_configs_data = evaluation_config_data.orch_config_data + + if deployment_status != Status.RUNNING: + error_collector.add_error( + ErrorCode.INVALID_DEPLOYMENT_STATUS, + f"Deployment status is '{deployment_status}', expected 'RUNNING'.", + ) + return + + fetch_and_validate_orchestration_config( + ai_core_client, config_id, orch_configs_data, resource_group, error_collector + ) + model_name, model_version = select_model_details_randomly( + orch_configs_data, error_collector + ) + test_orch_config = update_test_orch_config( + model_name, model_version, error_collector + ) + call_orchestration_service_with_v2_config( + test_orch_config, + ai_core_client, + orchestration_url, + resource_group, + error_collector, + proxy_client, + ) + + +def validate_orchestration_configuration( + orchestration_config_data: List[dict], + error_collector: ValidationCollector, +): + """Validates the Orchestration configuration provided by user""" + for orch_config in orchestration_config_data: + validate_orch_config_mandatory_modules(orch_config, error_collector) + validate_model_name_in_llm_module_config(orch_config, error_collector) + validate_template_ref_absent_in_config(orch_config, error_collector) + validate_if_template_list_is_empty_in_templating_module_config( + orch_config, error_collector + ) + validate_if_content_inside_template_is_empty_in_templating_module_config( + orch_config, error_collector + ) + validate_if_image_url_is_provided_in_content_type_inside_templating_module_config( + orch_config, error_collector + ) + validate_if_grounding_output_present_in_prompt_variables( + orch_config, error_collector + ) + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, error_collector + ) + + +def validate_input_config( + orchestration_config_data: List[dict], + metrics: List[str], + metric_templates: List[dict], + error_collector: ValidationCollector, +): + """Validates the input parameters of run data and metrics""" + # Validates the metrics of the run data + validate_metrics( + metrics, metric_templates, orchestration_config_data, error_collector + ) + # Validates the Orchestration configuration provided by user + validate_orchestration_configuration(orchestration_config_data, error_collector) + + remove_filter_metrics_if_provider_not_supported( + orchestration_config_data, metrics, error_collector + ) + + +def validate_variable_mapping_with_input_config( + orchestration_config_data: List[dict], + dataset_data: dict, + variable_mapping: dict, + metrics: List[str], + metric_templates: List[dict], + error_collector: ValidationCollector, +): + """ + Validates all the required variable mappings provided in input config with a zero-tolerance failure threshold. + + Args: + orchestration_config_data(list): Orchestration run configuration + dataset_data (dict): Dataset rows to validate + variable_mapping (dict): The variable mapping provided in the input configuration. + metrics (list[str]): List of metrics provided in the input configuration. + metric_templates (list[dict]): Metric templates information resolved from Metric Management Service + error_collector (ValidationCollector): To accumulate the errors occurred during the process + Raises: + ValidationError: If any required variable mapping is invalid or the default column does not exist in the dataset. + """ + validate_variable_mapping_of_prompts( + orchestration_config_data, dataset_data, variable_mapping, error_collector + ) + validate_variable_mapping_of_metrics( + metrics, metric_templates, dataset_data, variable_mapping, error_collector + ) + + +def validate_config_data_collection( + accumulated_config_data: List[_EvaluationConfigData] | _EvaluationConfigData, + error_collector: ValidationCollector, +): + """wrapper function to perform validation of fetched config data in case of single vs multiple executions flow""" + items = ( + accumulated_config_data + if isinstance(accumulated_config_data, list) + else [accumulated_config_data] + ) + + for current_config_data in items: + validate_merged_config_data(current_config_data, error_collector) + + +def validate_merged_config_data( + evaluation_config_data: _EvaluationConfigData, + error_collector: ValidationCollector, +): + """handles the validation of config provided from the user""" + + orchestration_config_data = evaluation_config_data.orch_config_data + metrics = evaluation_config_data.metrics_list + dataset_data = evaluation_config_data.dataset_data + variable_mapping = evaluation_config_data.variable_mapping + metric_templates = evaluation_config_data.metric_templates + validate_input_config( + orchestration_config_data, metrics, metric_templates, error_collector + ) + # validates the variable mapping provided in the input config + validate_variable_mapping_with_input_config( + orchestration_config_data, + dataset_data, + variable_mapping, + metrics, + metric_templates, + error_collector, + ) + # validates whether the dependent variables for the metrics provided exist in the datset + handle_missing_dependent_variables_in_dataset( + dataset_data, metrics, metric_templates, variable_mapping, error_collector + ) + # validates whether the json_schema_match metric is provided in input config, then a valid schema needs to be provided + handle_json_schema_match(metrics, dataset_data, variable_mapping, error_collector) + # validates whether the language_match metric is provided in input config, then a valid schema needs to be provided + handle_language_match(metrics, dataset_data, variable_mapping, error_collector) + # handles the case where a single reference is provided in the input config, treats it as a golden reference and populates all the rows of test data + handle_reference_missing_rows( + dataset_data, variable_mapping, metrics, error_collector + ) + return diff --git a/packages/gen/gen_ai_hub/orchestration/__init__.py b/packages/gen/gen_ai_hub/orchestration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/orchestration/exceptions.py b/packages/gen/gen_ai_hub/orchestration/exceptions.py new file mode 100644 index 0000000..822b0f2 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/exceptions.py @@ -0,0 +1,47 @@ +import httpx +from typing import Dict, Any + + +class OrchestrationError(Exception): + """ + This exception is raised when an error occurs during the execution of the + orchestration service, typically due to incorrect usage, invalid configurations, + or issues with run parameters defined by the user. + """ + + def __init__( + self, + request_id: str, + http_headers: httpx.Headers, + message: str, + code: int, + location: str, + module_results: Dict[str, Any], + retries: int = 0, + ): + """The constructor for OrchestrationError class. + + :param request_id: unique identifier for the request + :type request_id: str + :param http_headers: the HTTP headers associated with the error, useful in case of e.g. rate limiting. + :type http_headers: httpx.Headers + :param message: Detailed error message describing the issue. + :type message: str + :param code: Error code associated with the specific type of failure + :type code: int + :param location: Specific component or step in the orchestration process where the error occurred + :type location: str + :param module_results: State information and partial results from various modules at the time of the error, + useful for debugging + :type module_results: Dict[str, Any] + :param retries: the number of retries attempted + :type retries: int, optional + """ + self.request_id = request_id + self.http_headers = http_headers + self.message = message + self.code = code + self.location = location + self.module_results = module_results + self.retries = retries + super().__init__(message) diff --git a/packages/gen/gen_ai_hub/orchestration/models/__init__.py b/packages/gen/gen_ai_hub/orchestration/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/orchestration/models/azure_content_filter.py b/packages/gen/gen_ai_hub/orchestration/models/azure_content_filter.py new file mode 100644 index 0000000..67ec04a --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/azure_content_filter.py @@ -0,0 +1,74 @@ +from enum import Enum +from typing import Union, Literal + +from gen_ai_hub.orchestration.models.content_filter import ContentFilter, ContentFilterProvider + + +class AzureThreshold(int, Enum): + """ + Enumerates the threshold levels for the Azure Content Safety service. + + This enum defines the various threshold levels that can be used to filter + content based on its safety score. Each threshold value represents a specific + level of content moderation. + + Values: + ALLOW_SAFE: Allows only Safe content. + + ALLOW_SAFE_LOW: Allows Safe and Low content. + + ALLOW_SAFE_LOW_MEDIUM: Allows Safe, Low, and Medium content. + + ALLOW_ALL: Allows all content (Safe, Low, Medium, and High). + """ + + ALLOW_SAFE = 0 + ALLOW_SAFE_LOW = 2 + ALLOW_SAFE_LOW_MEDIUM = 4 + ALLOW_ALL = 6 + + +class AzureContentFilter(ContentFilter): + """ + Specific implementation of ContentFilter for Azure's content filtering service. + + This class configures content filtering based on Azure's categories and + severity levels. It allows setting thresholds for hate speech, sexual content, + violence, and self-harm content. + """ + + def __init__( + self, + hate: Union[AzureThreshold, Literal[0, 2, 4, 6]], + sexual: Union[AzureThreshold, Literal[0, 2, 4, 6]], + violence: Union[AzureThreshold, Literal[0, 2, 4, 6]], + self_harm: Union[AzureThreshold, Literal[0, 2, 4, 6]], + **kwargs + ): + """Initializes the AzureContentFilter with specified thresholds for different content categories. + + :param hate: threshold for hate speech content + :type hate: Union[AzureThreshold, Literal[0, 2, 4, 6]] + :param sexual: threshold for sexual content + :type sexual: Union[AzureThreshold, Literal[0, 2, 4, 6]] + :param violence: threshold for violent content + :type violence: Union[AzureThreshold, Literal[0, 2, 4, 6]] + :param self_harm: threshold for self-harm content + :type self_harm: Union[AzureThreshold, Literal[0, 2, 4, 6]] + """ + + hate = hate if isinstance(hate, AzureThreshold) else AzureThreshold(hate) + sexual = sexual if isinstance(sexual, AzureThreshold) else AzureThreshold(sexual) + violence = violence if isinstance(violence, AzureThreshold) else AzureThreshold(violence) + self_harm = self_harm if isinstance(self_harm, AzureThreshold) else AzureThreshold(self_harm) + + super().__init__( + provider=ContentFilterProvider.AZURE, + config={ + "Hate": hate, + "Sexual": sexual, + "Violence": violence, + "SelfHarm": self_harm, + **kwargs + }, + ) diff --git a/packages/gen/gen_ai_hub/orchestration/models/base.py b/packages/gen/gen_ai_hub/orchestration/models/base.py new file mode 100644 index 0000000..32de9fb --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/base.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any + + +class JSONSerializable(ABC): + """ + An interface for objects that can be serialized to JSON. + """ + + @abstractmethod + def to_dict(self) -> Dict[str, Any]: + """Convert the object to a JSON-serializable dictionary. + + :return: dictionary representation of the object. + :rtype: Dict[str, Any] + """ + + pass diff --git a/packages/gen/gen_ai_hub/orchestration/models/config.py b/packages/gen/gen_ai_hub/orchestration/models/config.py new file mode 100644 index 0000000..88d5d70 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/config.py @@ -0,0 +1,98 @@ +from typing import Optional, Union + +from gen_ai_hub.orchestration.models.base import JSONSerializable +from gen_ai_hub.orchestration.models.content_filtering import ContentFiltering +from gen_ai_hub.orchestration.models.data_masking import DataMasking +from gen_ai_hub.orchestration.models.document_grounding import GroundingModule +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.template import Template +from gen_ai_hub.orchestration.models.template_ref import TemplateRef +from gen_ai_hub.orchestration.models.translation.translation import Translation + + +class OrchestrationConfig(JSONSerializable): + """ + Configuration for the Orchestration Service's content generation process. + + Defines modules for a harmonized API that combines LLM-based content generation + with additional processing functionalities. + + The orchestration service allows for advanced content generation by processing inputs through a series of steps: + template rendering, text generation via LLMs, and optional input/output transformations such as data masking + or filtering. + """ + + def __init__( + self, + template: Union[Template, TemplateRef], + llm: LLM, + filtering: Optional[ContentFiltering] = None, + data_masking: Optional[DataMasking] = None, + grounding: Optional[GroundingModule] = None, + stream_options: Optional[dict] = None, + translation: Optional[Translation] = None, + ): + """Initializes the OrchestrationConfig with specified modules. + + :param template: template for rendering input prompts + :type template: Union[Template, TemplateRef] + :param llm: language model for text generation + :type llm: LLM + :param filtering: content filtering module, defaults to None + :type filtering: Optional[ContentFiltering], optional + :param data_masking: data masking module, defaults to None + :type data_masking: Optional[DataMasking], optional + :param grounding: document grounding module, defaults to None + :type grounding: Optional[GroundingModule], optional + :param stream_options: global streaming options, defaults to None + :type stream_options: Optional[dict], optional + :param translation: translation module, defaults to None + :type translation: Optional[Translation], optional + """ + + self.template = template + self.llm = llm + self.filtering = filtering + self.data_masking = data_masking + self.grounding = grounding + self.stream_options = stream_options + self._stream = False + self.translation = translation + + def _get_module_configurations(self): + configs = { + "templating_module_config": self.template.to_dict(), + "llm_module_config": self.llm.to_dict(), + } + + if self.data_masking: + configs["masking_module_config"] = self.data_masking.to_dict() + + if self.filtering: + configs["filtering_module_config"] = self.filtering.to_dict() + + if self.grounding: + configs["grounding_module_config"] = self.grounding.to_dict() + + if self.translation: + if self.translation.input_translation: + configs["input_translation_module_config"] = self.translation.input_translation.to_dict() + if self.translation.output_translation: + configs["output_translation_module_config"] = self.translation.output_translation.to_dict() + + return configs + + def to_dict(self): + """Converts the orchestration configuration to a dictionary format. + + :return: dictionary representation of the orchestration configuration. + :rtype: dict + """ + + config = { + "module_configurations": self._get_module_configurations(), + **({"stream": True} if self._stream else {}), + **({"stream_options": self.stream_options} if self._stream and self.stream_options else {}) + } + + return config diff --git a/packages/gen/gen_ai_hub/orchestration/models/content_filter.py b/packages/gen/gen_ai_hub/orchestration/models/content_filter.py new file mode 100644 index 0000000..aeede70 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/content_filter.py @@ -0,0 +1,51 @@ +from enum import Enum +from typing import Union + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class ContentFilterProvider(str, Enum): + """ + Enumerates supported content filter providers. + + This enum defines the available content filtering services that can be used + for content moderation tasks. Each enum value represents a specific provider. + + Values: + AZURE: Represents the Azure Content Safety service. + + LLAMA_GUARD_3_8B: Represents the Llama Guard 3 based on Llama-3.1-8B pretrained model. + """ + + AZURE = "azure_content_safety" + LLAMA_GUARD_3_8B = "llama_guard_3_8b" + + +class ContentFilter(JSONSerializable): + """ + Base class for content filtering configurations. + + This class provides a generic structure for defining content filters + from various providers. It allows for specifying the provider and + associated configuration parameters. + """ + + def __init__(self, provider: Union[ContentFilterProvider, str], config: dict): + """Initializes the ContentFilter with specified provider and configuration. + + :param provider: The name of the content filter provider. + :type provider: Union[ContentFilterProvider, str] + :param config: A dictionary containing the configuration parameters for the content filter. + :type config: dict + """ + self.provider = provider + self.config = config + + def to_dict(self): + """to_dict method to convert the content filter to a dictionary. + + :return: dictionary representation of the content filter. + :rtype: dict + """ + return {"type": self.provider, "config": self.config} + diff --git a/packages/gen/gen_ai_hub/orchestration/models/content_filtering.py b/packages/gen/gen_ai_hub/orchestration/models/content_filtering.py new file mode 100644 index 0000000..5c009b8 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/content_filtering.py @@ -0,0 +1,95 @@ +from typing import List, Optional + +from gen_ai_hub.orchestration.models.base import JSONSerializable +from gen_ai_hub.orchestration.models.content_filter import ContentFilter + + +class InputFiltering(JSONSerializable): + """Module for managing and applying input content filters.""" + + def __init__( + self, + filters: List[ContentFilter] + ): + """Initializes the InputFiltering with specified filters. + + :param filters: List of ContentFilter objects to be applied to input content. + :type filters: List[ContentFilter] + """ + self.filters = filters + + def to_dict(self): + """to_dict method to convert the input filtering configuration to a dictionary. + + :return: dictionary representation of the input filtering configuration. + :rtype: dict + """ + return { + "filters": [f.to_dict() for f in self.filters], + } + + +class OutputFiltering(JSONSerializable): + """Module for managing and applying output content filters.""" + + def __init__(self, + filters: List[ContentFilter], + stream_options: Optional[dict] = None + ): + """Initializes the OutputFiltering with specified filters and optional streaming options. + + :param filters: List of ContentFilter objects to be applied to output content. + :type filters: List[ContentFilter] + :param stream_options: Module-specific streaming options, defaults to None + :type stream_options: Optional[dict], optional + """ + self.filters = filters + self.stream_options = stream_options + + def to_dict(self): + """to_dict method to convert the output filtering configuration to a dictionary. + + :return: dictionary representation of the output filtering configuration. + :rtype: dict + """ + + config = { + "filters": [f.to_dict() for f in self.filters], + } + + if self.stream_options: + config["stream_options"] = self.stream_options + + return config + +class ContentFiltering(JSONSerializable): + """Module for managing and applying content filters.""" + + def __init__( + self, + input_filtering: Optional[InputFiltering] = None, + output_filtering: Optional[OutputFiltering] = None + ): + """Initializes the ContentFiltering with optional input and output filtering configurations. + + :param input_filtering: the configuration for input filtering, defaults to None + :type input_filtering: Optional[InputFiltering], optional + :param output_filtering: the configuration for output filtering, defaults to None + :type output_filtering: Optional[OutputFiltering], optional + """ + self.input_filtering = input_filtering + self.output_filtering = output_filtering + + def to_dict(self): + """to_dict method to convert the content filtering configuration to a dictionary. + + :return: dictionary representation of the content filtering configuration. + :rtype: dict + """ + config = {} + if self.input_filtering: + config["input"] = self.input_filtering.to_dict() + if self.output_filtering: + config["output"] = self.output_filtering.to_dict() + + return config diff --git a/packages/gen/gen_ai_hub/orchestration/models/data_masking.py b/packages/gen/gen_ai_hub/orchestration/models/data_masking.py new file mode 100644 index 0000000..22b62fc --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/data_masking.py @@ -0,0 +1,64 @@ +from abc import ABC +from enum import Enum +from typing import List + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class DataMaskingProviderName(str, Enum): + """ + Enumerates the available data masking providers. + + This enum defines the supported providers for masking sensitive data in the LLM module. + + Values: SAP_DATA_PRIVACY_INTEGRATION: Refers to the SAP Data Privacy Integration service, which offers + anonymization and pseudonymization capabilities for sensitive data. + """ + SAP_DATA_PRIVACY_INTEGRATION = "sap_data_privacy_integration" + + +class DataMaskingProvider(JSONSerializable, ABC): + """ + Abstract base class for data masking providers. + + This class serves as a blueprint for implementing different data masking providers. Each provider is responsible + for masking sensitive or personally identifiable information (PII) according to a specific method. + + Inherited by: + - SAPDataPrivacyIntegration + """ + pass + + +class DataMasking(JSONSerializable): + """ + Manages data masking operations using the specified providers. + + The DataMasking class is responsible for configuring and executing data masking processes + by delegating to one or more data masking providers. It supports either anonymization or pseudonymization + of sensitive information, depending on the provider and method used. + """ + + def __init__(self, providers: List[DataMaskingProvider]): + """Initializes the DataMasking instance with the specified providers. + + :param providers: A list of data masking providers. + :type providers: List[DataMaskingProvider] + :raises ValueError: If more than one provider is specified, as multiple providers + are not supported in the current version. + """ + + if len(providers) > 1: + raise ValueError("Multiple data masking providers are not supported in the current version.") + + self.providers = providers + + def to_dict(self): + """Converts the DataMasking instance to a dictionary representation. + + :return: A dictionary containing the data masking providers. + :rtype: dict + """ + return { + "masking_providers": [provider.to_dict() for provider in self.providers] + } diff --git a/packages/gen/gen_ai_hub/orchestration/models/document_grounding.py b/packages/gen/gen_ai_hub/orchestration/models/document_grounding.py new file mode 100644 index 0000000..a545476 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/document_grounding.py @@ -0,0 +1,214 @@ +from enum import Enum +from typing import List, Dict, Any + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class GroundingType(str, Enum): + """ + Enumerates supported grounding types. + """ + DOCUMENT_GROUNDING_SERVICE = "document_grounding_service" + + +class DataRepositoryType(str, Enum): + """ + Enumerates data repository types. + """ + VECTOR = "vector" # DataRepository with vector embeddings + URL = "help.sap.com" # website supporting elastic search + + +class DocumentMetadata(JSONSerializable): + """Restrict documents considered during search to those annotated with the given metadata.""" + + def __init__(self, key: str, value: List[str], select_mode: List[str] = None): + """Initializes the DocumentMetadata instance. + + :param key: The key for the metadata. + :type key: str + :param value: The list of values for the metadata. + :type value: List[str] + :param select_mode: Select mode for search filters. + :type select_mode: List[str], optional + """ + self.key = key + self.value = value + self.select_mode = select_mode + + def to_dict(self): + """Converts the DocumentMetadata instance to a dictionary representation. + + :return: Dictionary representation of the DocumentMetadata. + :rtype: dict + """ + config = {"key": self.key, "value": self.value} + if self.select_mode: + config["select_mode"] = self.select_mode + + return config + + +class GroundingFilterSearch(JSONSerializable): + """Search configuration for the data repository.""" + + def __init__(self, max_chunk_count: int = None, max_document_count: int = None): + """Initializes the GroundingFilterSearch instance. + + :param max_chunk_count: maximum number of chunks > 0 to return, defaults to None + :type max_chunk_count: int, optional + :param max_document_count: Maximum number of documents > 0 to return. + Only supports 'vector' dataRepositoryType. Cannot be used with 'maxChunkCount'. + If maxDocumentCount is given, then only one chunk per document is returned, defaults to None + :type max_document_count: int, optional + :raises ValueError: If both max_chunk_count and max_document_count are set. + """ + self.max_chunk_count = max_chunk_count + self.max_document_count = max_document_count + if self.max_chunk_count and self.max_document_count: + raise ValueError("Cannot set both max_chunk_count and max_document_count") + + def to_dict(self): + """Converts the GroundingFilterSearch instance to a dictionary representation. + + :return: Dictionary representation of the GroundingFilterSearch. + :rtype: dict + """ + config = {} + if self.max_chunk_count: + config["max_chunk_count"] = self.max_chunk_count + if self.max_document_count: + config["max_document_count"] = self.max_document_count + return config + + +class DocumentGroundingFilter(JSONSerializable): + """Module for configuring document grounding filters.""" + + def __init__(self, + id: str, + data_repository_type: str, + search_config: GroundingFilterSearch = None, + data_repositories: List[str] = None, + data_repository_metadata: List[Dict[str, Any]] = None, + document_metadata: List[DocumentMetadata] = None, + chunk_metadata: List[Dict[str, Any]] = None + ): + """Initializes the DocumentGroundingFilter instance. + + :param id: The unique identifier for the grounding filter. + :type id: str + :param data_repository_type: Only include DataRepositories with the given type: + 'vector' or 'url' of website supporting elastic search. + :type data_repository_type: str + :param search_config: GroundingFilterSearchConfiguration object, defaults to None + :type search_config: GroundingFilterSearch, optional + :param data_repositories: list of data repositories to search. + Specify ['*'] to search across all DataRepositories or + give a specific list of DataRepository ids, defaults to None + :type data_repositories: List[str], optional + :param data_repository_metadata: The metadata for the data repository, + Restrict DataRepositories considered during search to those annotated with the given + metadata. Useful when combined with dataRepositories=['*'], defaults to None + :type data_repository_metadata: List[Dict[str, Any]], optional + :param document_metadata: DocumentMetadata object, defaults to None + :type document_metadata: List[DocumentMetadata], optional + :param chunk_metadata: Restrict chunks considered during search to those with the given metadata, + defaults to None + :type chunk_metadata: List[Dict[str, Any]], optional + """ + + self.id = id + self.data_repository_type = data_repository_type + self.search_config = search_config + self.data_repositories = data_repositories + self.data_repository_metadata = data_repository_metadata + self.document_metadata = document_metadata + self.chunk_metadata = chunk_metadata + + def to_dict(self): + """Converts the DocumentGroundingFilter instance to a dictionary representation. + + :return: Dictionary representation of the DocumentGroundingFilter. + :rtype: dict + """ + config = { + "id": self.id, + "data_repository_type": self.data_repository_type, + } + if self.search_config: + config["search_config"] = self.search_config.to_dict() + if self.data_repositories: + config["data_repositories"] = self.data_repositories + if self.data_repository_metadata: + config["data_repository_metadata"] = self.data_repository_metadata + if self.document_metadata: + config["document_metadata"] = [metadata.to_dict() for metadata in self.document_metadata] + if self.chunk_metadata: + config["chunk_metadata"] = self.chunk_metadata + return config + + +class DocumentGrounding(JSONSerializable): + """defines the detailed configuration for the Grounding module.""" + + def __init__(self, input_params: List[str], output_param: str, filters: List[DocumentGroundingFilter] = None, + metadata_params: List[str] = None): + """Initializes the DocumentGrounding instance. + + :param input_params: The list of input parameters used for grounding input questions. + :type input_params: List[str] + :param output_param: Parameter name used for grounding output. + :type output_param: str + :param filters: List of DocumentGroundingFilter objects, defaults to None + :type filters: List[DocumentGroundingFilter], optional + :param metadata_params: Parameter name used for specifying metadata parameters, defaults to None + :type metadata_params: List[str], optional + """ + self.input_params = input_params + self.output_param = output_param + self.filters = filters + self.metadata_params = metadata_params + + def to_dict(self): + """Converts the DocumentGrounding instance to a dictionary representation. + + :return: Dictionary representation of the DocumentGrounding. + :rtype: dict + """ + config = { + "input_params": self.input_params, + "output_param": self.output_param, + } + if self.filters: + config["filters"] = [filter.to_dict() for filter in self.filters] + if self.metadata_params: + config["metadata_params"] = self.metadata_params + + return config + + +class GroundingModule(JSONSerializable): + """Module for managing and applying grounding aka RAG configurations.""" + + def __init__(self, type: str, config: DocumentGrounding): + """Initializes the GroundingModule instance. + + :param type: The type of the grounding module. + :type type: str + :param config: Configuration for the grounding module. + :type config: DocumentGrounding + """ + self.type = type + self.config = config + + def to_dict(self): + """Converts the GroundingModule instance to a dictionary representation. + + :return: Dictionary representation of the GroundingModule. + :rtype: dict + """ + return { + "type": self.type, + "config": self.config.to_dict(), + } diff --git a/packages/gen/gen_ai_hub/orchestration/models/llama_guard_3_filter.py b/packages/gen/gen_ai_hub/orchestration/models/llama_guard_3_filter.py new file mode 100644 index 0000000..754dccd --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/llama_guard_3_filter.py @@ -0,0 +1,88 @@ +from gen_ai_hub.orchestration.models.content_filter import ContentFilter, ContentFilterProvider + + +class LlamaGuard38bFilter(ContentFilter): + """ + Specific implementation of ContentFilter for Llama Guard 3. Llama Guard 3 is a + Llama-3.1-8B pretrained model, fine-tuned for content safety classification. + """ + + def __init__( + self, + violent_crimes: bool = False, + non_violent_crimes: bool = False, + sex_crimes: bool = False, + child_exploitation: bool = False, + defamation: bool = False, + specialized_advice: bool = False, + privacy: bool = False, + intellectual_property: bool = False, + indiscriminate_weapons: bool = False, + hate: bool = False, + self_harm: bool = False, + sexual_content: bool = False, + elections: bool = False, + code_interpreter_abuse: bool = False, + **kwargs): # pylint: disable=too-many-arguments + """Initializes the LlamaGuard38bFilter with specified content filtering options. + + :param violent_crimes: Responses that enable, encourage, or endorse the commission of violent crimes. + Defaults to False. + :type violent_crimes: bool, optional + :param non_violent_crimes: Responses that enable, encourage, or endorse the commission of non-violent crimes. + Defaults to False. + :type non_violent_crimes: bool, optional + :param sex_crimes: Responses that enable, encourage, or endorse the commission of sex-related crimes. + Defaults to False. + :type sex_crimes: bool, optional + :param child_exploitation: Responses that contain, describe, enable or endorse the sexual abuse of children. + Defaults to False. + :type child_exploitation: bool, optional + :param defamation: Responses that are both verifiably false and likely to injure a living person's reputation. + Defaults to False. + :type defamation: bool, optional + :param specialized_advice: Responses that contain specialized financial, medical or legal advice. + Defaults to False. + :type specialized_advice: bool, optional + :param privacy: Responses that contain sensitive or nonpublic personal information. Defaults to False. + :type privacy: bool, optional + :param intellectual_property: Responses that may violate the intellectual property rights of any third party. + Defaults to False. + :type intellectual_property: bool, optional + :param indiscriminate_weapons: Responses that enable, encourage, or endorse the creation of indiscriminate + weapons. Defaults to False. + :type indiscriminate_weapons: bool, optional + :param hate: Responses that demean or dehumanize people on the basis of their sensitive, + personal characteristics. Defaults to False. + :type hate: bool, optional + :param self_harm: Responses that enable, encourage, or endorse acts of intentional self-harm. Defaults to False. + :type self_harm: bool, optional + :param sexual_content: Responses that contain erotica. Defaults to False. + :type sexual_content: bool, optional + :param elections: Responses that contain factually incorrect information about electoral systems and processes. + Defaults to False. + :type elections: bool, optional + :param code_interpreter_abuse: Responses that seek to abuse code interpreters. Defaults to False. + :type code_interpreter_abuse: bool, optional + """ + + super().__init__( + provider=ContentFilterProvider.LLAMA_GUARD_3_8B, + config={ + "violent_crimes": violent_crimes, + "non_violent_crimes": non_violent_crimes, + "sex_crimes": sex_crimes, + "child_exploitation": child_exploitation, + "defamation": defamation, + "specialized_advice": specialized_advice, + "privacy": privacy, + "intellectual_property": intellectual_property, + "indiscriminate_weapons": indiscriminate_weapons, + "hate": hate, + "self_harm": self_harm, + "sexual_content": sexual_content, + "elections": elections, + "code_interpreter_abuse": code_interpreter_abuse, + **kwargs + } + ) diff --git a/packages/gen/gen_ai_hub/orchestration/models/llm.py b/packages/gen/gen_ai_hub/orchestration/models/llm.py new file mode 100644 index 0000000..98013b6 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/llm.py @@ -0,0 +1,54 @@ +from typing import Optional, Dict + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class LLM(JSONSerializable): + """ + Represents a Large Language Model (LLM) configuration. + + This class encapsulates the details required to specify and configure a particular + LLM for use in natural language processing tasks. It includes the model's name, + version, and any additional parameters needed for its operation. + """ + + def __init__( + self, + name: str, + version: str = "latest", + parameters: Optional[Dict] = None, + ): + """Initializes the LLM with specified name, version, and parameters. + + :param name: Name of the LLM. + :type name: str + :param version: Version of the LLM, defaults to "latest" + :type version: str, optional + :param parameters: Additional parameters for the LLM, defaults to None + + Common parameters include: + + - 'temperature': Controls randomness in output. Lower values (e.g., 0.2) + make output more focused and deterministic, while higher values (e.g., 0.8) + make output more diverse and creative. + + - 'max_tokens': Sets the maximum number of tokens to generate in the response. + This can help control the length of the model's output. + + :type parameters: Optional[Dict], optional + """ + self.name = name + self.version = version + self.parameters = parameters or {} + + def to_dict(self): + """Converts the LLM instance to a dictionary representation. + + :return: Dictionary representation of the LLM. + :rtype: dict + """ + return { + "model_name": self.name, + "model_version": self.version, + "model_params": self.parameters, + } diff --git a/packages/gen/gen_ai_hub/orchestration/models/message.py b/packages/gen/gen_ai_hub/orchestration/models/message.py new file mode 100644 index 0000000..e970e99 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/message.py @@ -0,0 +1,239 @@ +import json +import typing +from dataclasses import dataclass, field +from enum import Enum +from typing import Union, Optional, List + +from gen_ai_hub.orchestration.models.base import JSONSerializable +from gen_ai_hub.orchestration.models.multimodal_items import ContentPart, ImageItem, TextPart, ImageUrl, ImagePart + +@dataclass +class FunctionCall: + """ + The function that the model called. + """ + name: Optional[str] = field( + default=None, + metadata={"description": "The name of the function to call."} + ) + arguments: Optional[str] = field( + default=None, + metadata={ + "description": ( + "The arguments to call the function with, as generated by the " + "model in JSON format. Note that the model does not always " + "generate valid JSON, and may hallucinate parameters not " + "defined by your function schema. Validate the arguments in " + "your code before calling your function." + ) + } + ) + + def parse_arguments(self) -> dict: + """Parses the arguments string as JSON. + + :return: A dictionary representing the parsed arguments. + :rtype: dict + """ + if self.arguments is None: + return {} + + return json.loads(self.arguments) + + +@dataclass +class MessageToolCall: + """ + Represents a tool call within a message, specifically a function call. + """ + id: str = field(metadata={"description": "The ID of the tool call."}) + type: typing.Literal["function"] = field( + metadata={ + "description": ( + "The type of the tool. Currently, only `function` is supported." + ) + } + ) + function: FunctionCall = field( + metadata={"description": "The function that the model called."} + ) + + def to_dict(self): + """Converts the MessageToolCall instance to a dictionary. + + :return: A dictionary representation of the MessageToolCall instance. + :rtype: dict + """ + return { + "id": self.id, + "type": self.type, + "function": { + "name": self.function.name, + "arguments": self.function.arguments, + } + } + + +class Role(str, Enum): + """ + Enumerates supported roles in LLM-based conversations. + + This enum defines the standard roles used in interactions with Large Language Models (LLMs). + These roles are generally used to structure the input and distinguish between different parts of the conversation. + + Values: + + - USER: Represents the human user's input in the conversation. + + - SYSTEM: Represents system-level instructions or context setting for the LLM. + + - ASSISTANT: Represents the LLM's responses in the conversation. + + - TOOL: Represents a tool or function that the LLM can call. + + - DEVELOPER: Represents the developer's input or instructions in the conversation. + + """ + + USER = "user" + SYSTEM = "system" + ASSISTANT = "assistant" + TOOL = "tool" + DEVELOPER = "developer" + +@dataclass +class Message(JSONSerializable): + """ + Represents a single message in a prompt or conversation template. + + This base class defines the structure for all types of messages in a prompt, + including content and role. + + Args: + role: The role of the entity sending the message. + + content: The message content, which may be plain text or a sequence of text and images. + """ + + role: Union[Role, str] + content: Union[str, List[ContentPart]] + refusal: Optional[str] = None + tool_calls: Optional[List[MessageToolCall]] = None + + def to_dict(self): + """Converts the Message instance to a dictionary. + + :return: A dictionary representation of the Message instance. + :rtype: dict + """ + base = { + "role": self.role, + "content": self.content if isinstance(self.content, str) else [item.to_dict() for item in self.content], + } + + if self.refusal is not None: + base["refusal"] = self.refusal + + if self.tool_calls: + base["tool_calls"] = [tool_call.to_dict() for tool_call in self.tool_calls] + + return base + + +class SystemMessage(Message): + """ + Represents a system message in a prompt or conversation template. + + System messages typically provide context or instructions to the AI model. + """ + + def __init__(self, content: str): + """Initializes a SystemMessage instance. + + :param content: The text content of the system message. + :type content: str + """ + super().__init__(role=Role.SYSTEM, content=content) + + +class UserMessage(Message): + """ + Represents a user message in a prompt or conversation template. + + User messages typically contain queries or inputs from the user. + """ + + def __init__(self, content: Union[str, List[Union[str, ImageItem]]]): + """Initializes a UserMessage instance. + + :param content: The message content, which may be plain text or a sequence of text and images. + :type content: Union[str, List[Union[str, ImageItem]]] + :raises TypeError: If the content list contains unsupported types. + """ + mapped_content = [] + + if isinstance(content, str): + mapped_content = content + elif isinstance(content, list): + for item in content: + if isinstance(item, str): + mapped_content.append(TextPart(text=item)) + elif isinstance(item, ImageItem): + mapped_content.append(ImagePart(image_url=ImageUrl(url=item.url, detail=item.detail))) + else: + raise TypeError("User message content list must contain only str or ImageItem") + + super().__init__(role=Role.USER, content=mapped_content) + + +class AssistantMessage(Message): + """ + Represents an assistant message in a prompt or conversation template. + + Assistant messages typically contain responses or outputs from the AI model. + """ + + def __init__( + self, + content: str, + refusal: Optional[str] = None, + tool_calls: Optional[List[MessageToolCall]] = None, + ): + """Initializes an AssistantMessage instance. + + :param content: The text content of the assistant message. + :type content: str + :param refusal: A string indicating refusal reason, defaults to None + :type refusal: Optional[str], optional + :param tool_calls: A list of tool call objects, defaults to None + :type tool_calls: Optional[List[MessageToolCall]], optional + """ + super().__init__(role=Role.ASSISTANT, content=content, refusal=refusal, tool_calls=tool_calls) + + +class ToolMessage(Message): + """Represents a tool message in a prompt or conversation template. + + :param Message: The text content of the tool message. + :type Message: str + """ + def __init__(self, content: str, tool_call_id: str): + """Initializes a ToolMessage instance. + + :param content: The text content of the tool message. + :type content: str + :param tool_call_id: The ID of the tool call associated with this message. + :type tool_call_id: str + """ + super().__init__(role=Role.TOOL, content=content) + self.tool_call_id = tool_call_id + + def to_dict(self): + """Converts the ToolMessage instance to a dictionary. + + :return: A dictionary representation of the ToolMessage instance. + :rtype: dict + """ + base = super().to_dict() + base["tool_call_id"] = self.tool_call_id + return base diff --git a/packages/gen/gen_ai_hub/orchestration/models/multimodal_items.py b/packages/gen/gen_ai_hub/orchestration/models/multimodal_items.py new file mode 100644 index 0000000..98a2bd0 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/multimodal_items.py @@ -0,0 +1,171 @@ +import base64 +import mimetypes +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Any, Optional, Union + +from gen_ai_hub.orchestration.models.base import JSONSerializable + +class ImageDetailLevel(Enum): + """ + Controls the resolution and detail level for image analysis. + + Attributes: + + - AUTO: The model determines the detail level automatically. + + - LOW: The model uses a low-fidelity, faster version of the image. + + - HIGH: The model uses a high-fidelity version of the image. + """ + AUTO = "auto" + LOW = "low" + HIGH = "high" + +@dataclass +class TextPart(JSONSerializable): + """ + Represents a text segment within a multimodal content block. + + Args: + + - text: The string content of the text part. + + - type: The type identifier, defaulting to "text". + """ + text: str + type: str = field(default="text") + + def to_dict(self): + """Converts the TextPart instance to a dictionary. + + :return: A dictionary representation of the TextPart. + :rtype: dict + """ + return { + "type": self.type, + "text": self.text, + } + + +@dataclass +class ImageUrl: + """ + A data structure holding the URL and detail level for an image. + + Args: + + - url: The location of the image, as a standard or data URL. + + - detail: The processing detail level for the image. + """ + url: str + detail: Optional[ImageDetailLevel] = None + + +@dataclass +class ImagePart(JSONSerializable): + """ + Represents an image segment within a multimodal content block. + + Args: + + - image_url: An `ImageUrl` object containing the image's location and detail level. + - type: The type identifier, defaulting to "image_url". + """ + image_url: ImageUrl + type: str = field(default="image_url") + + def to_dict(self): + """Converts the ImagePart instance to a dictionary. + + :return: A dictionary representation of the ImagePart. + :rtype: dict + """ + base = { + "type": self.type, + "image_url": { + "url": self.image_url.url, + }, + } + + if self.image_url.detail: + base["image_url"]["detail"] = self.image_url.detail + + return base + + +ContentPart = Union[TextPart, ImagePart] + + +class ImageItem(JSONSerializable): + """ + Represents an image for use in multimodal messages. + + Examples: + + Using a standard URL + img1 = ImageItem(url="https://example.com/image.png", detail=ImageDetailLevel.HIGH) + + Using a data URL + img2 = ImageItem(url="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...") + """ + + def __init__( + self, + url: Optional[str] = None, + detail: Optional[ImageDetailLevel] = None, + ): + """Initializes an ImageItem instance. + + :param url: The image location as a standard or data URL, defaults to None + + Standard URL example: 'https://example.com/image.png' + + Data URL example: 'data:image/png;base64,...' + :type url: Optional[str], optional + :param detail: The image detail level for model processing, defaults to None + :type detail: Optional[ImageDetailLevel], optional + """ + self.url = url + self.detail = detail + + @staticmethod + def from_file( + file_path: str, + mime_type: Optional[str] = None, + detail: Optional[ImageDetailLevel] = None, + ) -> "ImageItem": + """Creates an ImageItem from a local image file. + + :param file_path: Path to the image file. + :type file_path: str + :param mime_type: Explicit MIME type (e.g., 'image/png'). + If not provided, the MIME type will be guessed from the file extension. + :type mime_type: Optional[str], optional + :param detail: The image detail level for model processing. + :type detail: Optional[ImageDetailLevel], optional + :raises ValueError: If the MIME type cannot be determined and is not provided. + :raises FileNotFoundError: If the file does not exist. + :return: An ImageItem instance with the image data as a data URL. + :rtype: ImageItem + """ + mime = mime_type or mimetypes.guess_type(file_path)[0] + if not mime: + raise ValueError( + f"Could not determine MIME type for file: {file_path}. " + "Please provide mime_type explicitly." + ) + with open(file_path, "rb") as file: + encoded = base64.b64encode(file.read()).decode("utf-8") + data_url = f"data:{mime};base64,{encoded}" + + return ImageItem(url=data_url, detail=detail) + + def to_dict(self) -> Dict[str, Any]: + """Converts the ImageItem instance to a dictionary representation. + + :return: A dictionary representation of the ImageItem. + :rtype: Dict[str, Any] + """ + return ImagePart(image_url=ImageUrl(url=self.url, detail=self.detail)).to_dict() diff --git a/packages/gen/gen_ai_hub/orchestration/models/response.py b/packages/gen/gen_ai_hub/orchestration/models/response.py new file mode 100644 index 0000000..7c4489c --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/response.py @@ -0,0 +1,324 @@ +from dataclasses import dataclass +from typing import List, Optional, Dict, Any, Union + +from gen_ai_hub.orchestration.models.message import Message, FunctionCall +from gen_ai_hub.orchestration.models.multimodal_items import ContentPart + + +@dataclass +class ToolCallChunk: + """Represents a chunk of a tool call in a streaming chat response.""" + index: int + """The index of this tool call chunk in the sequence of chunks.""" + id: Optional[str] = None + """The unique identifier for the tool call.""" + type: Optional[str] = None + """The type of tool call, e.g., 'function' .""" + function: Optional[FunctionCall] = None + """The function call details associated with this tool call chunk.""" + + +@dataclass +class ChatDelta: + """Represents a partial update in a streaming chat response.""" + content: Union[str, List[ContentPart]] + """The text content of the chat delta.""" + role: Optional[str] = None + """Optional role identifier (e.g., 'assistant', 'user') for the message delta.""" + refusal: Optional[str] = None + """Optional refusal reason if the model refused to generate content.""" + tool_calls: Optional[List[ToolCallChunk]] = None + """Optional list of tool call chunks associated with this chat delta.""" + + +@dataclass +class LLMUsage: + """Represents the token usage statistics for an LLM (Large Language Model) operation. + """ + completion_tokens: int + """The number of tokens generated by the model in the response.""" + prompt_tokens: int + """The number of tokens in the input prompt.""" + total_tokens: int + """The total number of tokens used, including both prompt and completion tokens.""" + + +@dataclass +class LLMChoice: + """ + Represents an individual choice or response generated by the LLM. + + Attributes: + index: The index of this particular choice in the list of possible choices. + + message: The message object containing the role and content of the response. + + finish_reason: The reason why the model stopped generating tokens. + + logprobs: Optional dictionary containing token log probabilities. + """ + index: int + """The index of this particular choice in the list of possible choices.""" + message: Message + """The message object containing the role and content of the response.""" + finish_reason: str + """The reason why the model stopped generating tokens.""" + logprobs: Optional[Dict[str, float]] = None + """Optional dictionary containing token log probabilities.""" + + +@dataclass +class LLMChoiceStreaming: + """ + Represents a streaming choice or partial response generated by the LLM. + + Attributes: + index: The index of this particular choice in the list of possible choices. + + delta: The partial update (ChatDelta) for this choice. + + finish_reason: Optional reason for why the generation stopped, may be None during streaming. + + logprobs: Optional dictionary containing token log probabilities. + """ + index: int + """The index of this particular choice in the list of possible choices.""" + delta: ChatDelta + """The partial update (ChatDelta) for this choice.""" + finish_reason: Optional[str] = None + """Optional reason for why the generation stopped, may be None during streaming.""" + logprobs: Optional[Dict[str, float]] = None + """Optional dictionary containing token log probabilities.""" + + +@dataclass +class BaseLLMResult: + """ + Base class for LLM results containing common attributes. + + Attributes: + id: Unique identifier for the LLM operation. + object: Type of object returned (e.g., "chat.completion"). + created: Timestamp when this result was created. + model: Name or identifier of the model used. + """ + id: str + object: str + created: int + model: str + + +@dataclass +class LLMResult(BaseLLMResult): + """ + Represents the complete result from an LLM operation. + + Attributes: + id: The unique identifier for this LLM operation. + + object: The type of object returned (typically "chat.completion"). + + created: The timestamp when this result was created. + + model: The name or identifier of the model used for generating the result. + + choices: A list of possible choices generated by the LLM. + + usage: The token usage statistics for this operation. + + system_fingerprint: An optional system fingerprint for tracking the model used. + """ + choices: List[LLMChoice] + """A list of possible choices generated by the LLM.""" + usage: LLMUsage + """The token usage statistics for this operation.""" + system_fingerprint: Optional[str] = None + """An optional system fingerprint for tracking the model used.""" + + +@dataclass +class LLMResultStreaming(BaseLLMResult): + """ + Represents a streaming result from an LLM operation. + + Attributes: + id: The unique identifier for this LLM operation. + + object: The type of object returned (typically "chat.completion.chunk"). + + created: The timestamp when this result was created. + + model: The name or identifier of the model used. + + choices: A list of streaming choices generated by the LLM. + + usage: optional token usage statistics for this operation. + + system_fingerprint: An optional system fingerprint for tracking the model used. + """ + choices: List[LLMChoiceStreaming] + """A list of streaming choices generated by the LLM.""" + usage: Optional[LLMUsage] = None + """optional token usage statistics for this operation.""" + system_fingerprint: Optional[str] = None + """An optional system fingerprint for tracking the model used.""" + + +@dataclass +class GenericModuleResult: + """Represents a generic module result in the orchestration process.""" + + message: str + """A message or description generated by the module.""" + data: Optional[Dict[str, Any]] = None + """Additional data relevant to the module result.""" + + +@dataclass +class BaseModuleResults: + """ + Base class for module results containing grounding, common filtering and masking attributes. + + Attributes: + input_filtering: Results from the input filtering module. + + output_filtering: Results from the output filtering module. + + input_masking: Results from the input masking module. + + grounding: A list of extracted text to be provided as grounding context. + + input_translation: Results from the input translation module. + + output_translation: Results from the output translation module. + """ + input_filtering: Optional[GenericModuleResult] = None + """Results from the input filtering module.""" + output_filtering: Optional[GenericModuleResult] = None + """Results from the output filtering module.""" + input_masking: Optional[GenericModuleResult] = None + """Results from the input masking module.""" + grounding: Optional[GenericModuleResult] = None + """A list of extracted text to be provided as grounding context.""" + input_translation: Optional[GenericModuleResult] = None + """Results from the input translation module.""" + output_translation: Optional[GenericModuleResult] = None + """Results from the output translation module.""" + + +@dataclass +class ModuleResults(BaseModuleResults): + """ + Represents the results of various modules used in processing an orchestration request. + + Attributes: + templating: A list of messages that define the conversation's context or template. + + llm: The result from the LLM operation. + + input_filtering: The result of any input filtering, if applicable. + + output_filtering: The result of any output filtering, if applicable. + + input_masking: The result of input masking, if applicable. + + output_unmasking: The result of output unmasking, if applicable. + """ + llm: Optional[LLMResult] = None + """The result from the LLM operation.""" + templating: Optional[List[Message]] = None + """A list of messages that define the conversation's context or template.""" + output_unmasking: Optional[List[LLMChoice]] = None + """The result of output unmasking, if applicable.""" + + +@dataclass +class ModuleResultsStreaming(BaseModuleResults): + """ + Represents the streaming results of various modules used in processing an orchestration request. + + Attributes: + llm: The streaming result from the LLM operation. + + templating: A list of chat deltas that define the conversation's context or template. + + input_filtering: The result of any input filtering, if applicable. + + output_filtering: The result of any output filtering, if applicable. + + input_masking: The result of input masking, if applicable. + + output_unmasking: The result of output unmasking for streaming responses. + """ + llm: Optional[LLMResultStreaming] = None + """The streaming result from the LLM operation.""" + templating: Optional[List[ChatDelta]] = None + """A list of chat deltas that define the conversation's context or template.""" + output_unmasking: Optional[List[LLMChoiceStreaming]] = None + """The result of output unmasking for streaming responses.""" + + +@dataclass +class OrchestrationResponse: + """ + Represents the complete response from an orchestration process. + + Attributes: + request_id: The unique identifier for the request being processed. + + module_results: The results from the various modules involved in processing the request. + + orchestration_result: The final result from the orchestration, typically mirroring the LLM result. + """ + request_id: str + """The unique identifier for the request being processed.""" + module_results: ModuleResults + """The results from the various modules involved in processing the request.""" + orchestration_result: LLMResult + """The final result from the orchestration, typically mirroring the LLM result.""" + + @property + def content(self) -> str: + """Gets the content of the first choice in the orchestration result. + + :raises ValueError: If there are no choices available in the orchestration result. + :return: The content of the first choice. + :rtype: str + """ + + if not self.orchestration_result.choices: + raise ValueError("No choices available in the orchestration result.") + + return self.orchestration_result.choices[0].message.content + +@dataclass +class OrchestrationResponseStreaming: + """ + Represents the streaming response from an orchestration process. + + Attributes: + request_id: The unique identifier for the request being processed. + + module_results: The streaming results from the various modules involved in processing the request. + + orchestration_result: The streaming result from the orchestration. + """ + request_id: str + """The unique identifier for the request being processed.""" + module_results: ModuleResultsStreaming + """The streaming results from the various modules involved in processing the request.""" + orchestration_result: LLMResultStreaming + """The streaming result from the orchestration.""" + +@dataclass +class OrchestrationResponseWithRetries(OrchestrationResponse): + """ + Extended OrchestrationResponse that includes retry count information. + + This is returned when using retry-enabled methods like run_with_retries(). + + Attributes: + retries: Number of retry attempts that were made to successfully complete this request. + """ + retries: int = 0 + """Number of retry attempts that were made to successfully complete this request.""" diff --git a/packages/gen/gen_ai_hub/orchestration/models/response_format.py b/packages/gen/gen_ai_hub/orchestration/models/response_format.py new file mode 100644 index 0000000..66ad87b --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/response_format.py @@ -0,0 +1,151 @@ +import re +from enum import Enum +from typing import Optional + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class ResponseFormatType(str, Enum): + """ + Enumerates the supported response format. + + Response format that the model output should adhere to. This is the same as the OpenAI definition. + + Values: + TEXT: Response format as text + JSON_OBJECT: Response format as json object + JSON_SCHEMA: Response format as defined json schema + """ + TEXT = "text" + JSON_OBJECT = "json_object" + JSON_SCHEMA = "json_schema" + + +class ResponseFormatText(JSONSerializable): + """ + Response format that the model output should adhere to. + """ + + def to_dict(self): + """Converts the ResponseFormatText instance to a dictionary. + + :return: A dictionary representation of the ResponseFormatText. + :rtype: dict + """ + return {"type": ResponseFormatType.TEXT} + + +class ResponseFormatJsonObject(JSONSerializable): + """ + Response format JSON Object that the model output should adhere to. + """ + + def to_dict(self): + """Converts the ResponseFormatJsonObject instance to a dictionary. + + :return: A dictionary representation of the ResponseFormatJsonObject. + :rtype: dict + """ + return {"type": ResponseFormatType.JSON_OBJECT} + + +class ResponseFormatJsonSchema(JSONSerializable): + """ + Response format JSON Schema that the model output should adhere to. + """ + + def __init__( + self, + name, + schema: object = None, + description: Optional[str] = None, + strict: bool = False + ): + """Initializes a ResponseFormatJsonSchema instance. + + :param name: the name of the response format. + :type name: str + :param schema: the schema for the response format described as a JSON Schema object, defaults to None + :type schema: object, optional + :param description: A description of what the response format is for, defaults to None + :type description: Optional[str], optional + :param strict: Whether to enable strict schema adherence when generating the output, defaults to False + :type strict: bool, optional + """ + self.name = Validator.validate_name(name) + self.desciption = description + self.schema = schema + self.strict = strict + + def to_dict(self): + """Converts the ResponseFormatJsonSchema instance to a dictionary. + + :return: A dictionary representation of the ResponseFormatJsonSchema. + :rtype: dict + """ + json_schema = { + "name": self.name, + "strict": self.strict, + "schema": self.schema + } + if self.desciption: + json_schema['description'] = self.desciption + return { + "type": ResponseFormatType.JSON_SCHEMA, + "json_schema": json_schema + } + + +class ResponseFormatFactory(): + """ + Factory class that maps response format input to classes that can handle to_dict conversion. + """ + @staticmethod + def create_response_format_object(response_format): + """Creates a response format object based on the provided response format. + + :param response_format: The response format input. + :type response_format: Union[ResponseFormatType, ResponseFormatJsonSchema] + :return: An instance of the corresponding response format class. + :rtype: Optional[JSONSerializable] + """ + if response_format == ResponseFormatType.TEXT: + return ResponseFormatText() + + if response_format == ResponseFormatType.JSON_OBJECT: + return ResponseFormatJsonObject() + + if isinstance(response_format, ResponseFormatJsonSchema): + return response_format + + return None + + +class Validator(): + """ + A utility class for validating response format names. + + This class provides methods to validate the names of response formats to ensure + they adhere to specified patterns and length constraints. + """ + @staticmethod + def validate_name(name): + """Validates the name of the response format. + + :param name: The name to validate. + :type name: str + :raises ValueError: If the name does not match the required pattern or exceeds the maximum length. + :return: The validated name. + :rtype: str + """ + pattern = r'^[a-zA-Z0-9_-]+$' + if re.match(pattern, name): + if len(name) > 64: + raise ValueError("The name of the response format must be a-z, A-Z, 0-9, " + "or contain underscores and dashes, with a maximum length of 64.") + else: + raise ValueError("The name of the response format must be a-z, A-Z, 0-9, " + "or contain underscores and dashes, with a maximum length of 64.") + + return name + \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration/models/sap_data_privacy_integration.py b/packages/gen/gen_ai_hub/orchestration/models/sap_data_privacy_integration.py new file mode 100644 index 0000000..bc80e56 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/sap_data_privacy_integration.py @@ -0,0 +1,190 @@ +from enum import Enum +from typing import List + +from gen_ai_hub.orchestration.models.data_masking import DataMaskingProvider, DataMaskingProviderName + + +class MaskingMethod(str, Enum): + """ + Enumerates the supported masking methods. + + This enum defines the two main methods for masking sensitive information: anonymization and pseudonymization. + Anonymization irreversibly removes sensitive data, while pseudonymization allows the original data to be recovered. + + Values: + ANONYMIZATION: Irreversibly replaces sensitive data with placeholders (e.g., MASKED_ENTITY). + + PSEUDONYMIZATION: Replaces sensitive data with reversible placeholders (e.g., MASKED_ENTITY_ID). + """ + ANONYMIZATION = "anonymization" + """Irreversibly replaces sensitive data with placeholders (e.g., MASKED_ENTITY). """ + PSEUDONYMIZATION = "pseudonymization" + """Replaces sensitive data with reversible placeholders (e.g., MASKED_ENTITY_ID). """ + + +class ProfileEntity(str, Enum): + """ + Enumerates the entity categories that can be masked by the SAP Data Privacy Integration service. + + This enum lists different types of personal or sensitive information (PII) that can be detected and masked + by the data masking module, such as personal details, organizational data, contact information, and identifiers. + + Values: + PERSON: Represents personal names. + + ORG: Represents organizational names. + + UNIVERSITY: Represents educational institutions. + + LOCATION: Represents geographical locations. + + EMAIL: Represents email addresses. + + PHONE: Represents phone numbers. + + ADDRESS: Represents physical addresses. + + SAP_IDS_INTERNAL: Represents internal SAP identifiers. + + SAP_IDS_PUBLIC: Represents public SAP identifiers. + + URL: Represents URLs. + + USERNAME_PASSWORD: Represents usernames and passwords. + + NATIONAL_ID: Represents national identification numbers. + + IBAN: Represents International Bank Account Numbers. + + SSN: Represents Social Security Numbers. + + CREDIT_CARD_NUMBER: Represents credit card numbers. + + PASSPORT: Represents passport numbers. + + DRIVING_LICENSE: Represents driving license numbers. + + NATIONALITY: Represents nationality information. + + RELIGIOUS_GROUP: Represents religious group affiliation. + + POLITICAL_GROUP: Represents political group affiliation. + + PRONOUNS_GENDER: Represents pronouns and gender identity. + + GENDER: Represents gender information. + + SEXUAL_ORIENTATION: Represents sexual orientation. + + TRADE_UNION: Represents trade union membership. + + SENSITIVE_DATA: Represents any other sensitive information. + """ + + PERSON = "profile-person" + """Represents personal names. """ + ORG = "profile-org" + """Represents organizational names. """ + UNIVERSITY = "profile-university" + """Represents educational institutions. """ + LOCATION = "profile-location" + """Represents geographical locations. """ + EMAIL = "profile-email" + """Represents email addresses. """ + PHONE = "profile-phone" + """Represents phone numbers. """ + ADDRESS = "profile-address" + """Represents physical addresses. """ + SAP_IDS_INTERNAL = "profile-sapids-internal" + """Represents internal SAP identifiers. """ + SAP_IDS_PUBLIC = "profile-sapids-public" + """Represents public SAP identifiers. """ + URL = "profile-url" + """Represents URLs. """ + USERNAME_PASSWORD = "profile-username-password" + """Represents usernames and passwords. """ + NATIONAL_ID = "profile-nationalid" + """Represents national identification numbers. """ + IBAN = "profile-iban" + """Represents International Bank Account Numbers. """ + SSN = "profile-ssn" + """Represents Social Security Numbers. """ + CREDIT_CARD_NUMBER = "profile-credit-card-number" + """Represents credit card numbers. """ + PASSPORT = "profile-passport" + """Represents passport numbers. """ + DRIVING_LICENSE = "profile-driverlicense" + """Represents driving license numbers. """ + NATIONALITY = "profile-nationality" + """Represents nationality information. """ + RELIGIOUS_GROUP = "profile-religious-group" + """Represents religious group affiliation. """ + POLITICAL_GROUP = "profile-political-group" + """Represents political group affiliation. """ + PRONOUNS_GENDER = "profile-pronouns-gender" + """Represents pronouns and gender identity.""" + GENDER = "profile-gender" + """Represents gender information.""" + SEXUAL_ORIENTATION = "profile-sexual-orientation" + """Represents sexual orientation. """ + TRADE_UNION = "profile-trade-union" + """Represents trade union membership. """ + SENSITIVE_DATA = "profile-sensitive-data" + """Represents any other sensitive information. """ + + +class SAPDataPrivacyIntegration(DataMaskingProvider): + """ + SAP Data Privacy Integration provider for data masking. + + This class implements the SAP Data Privacy Integration service, which can anonymize or pseudonymize + specified entity categories in the input data. It supports masking sensitive information like personal names, + contact details, and identifiers. + """ + + def __init__( + self, + method: MaskingMethod, + entities: List[ProfileEntity], + allowlist: List[str] = None, + mask_grounding_input: bool = False, + ): + """Initializes the SAPDataPrivacyIntegration data masking provider. + + :param method: The method of masking to apply + :type method: MaskingMethod + :param entities: A list of entity categories to be masked + :type entities: List[ProfileEntity] + :param allowlist: A list of strings that should not be masked, defaults to None + :type allowlist: List[str], optional + :param mask_grounding_input: A flag indicating whether to mask input to the grounding module, defaults to False + :type mask_grounding_input: bool, optional + """ + self.method = method + self.entities = entities + self.allowlist = allowlist or [] + self.mask_grounding_input = mask_grounding_input + + def to_dict(self): + """Converts the SAPDataPrivacyIntegration instance to a dictionary representation. + + :return: Dictionary representation of the SAPDataPrivacyIntegration instance. + :rtype: dict + """ + result = { + "type": DataMaskingProviderName.SAP_DATA_PRIVACY_INTEGRATION, + "method": self.method, + "entities": [ + { + "type": entity + } for entity in self.entities + ], + "mask_grounding_input": { + "enabled": self.mask_grounding_input + } + } + + if self.allowlist: + result["allowlist"] = self.allowlist + + return result diff --git a/packages/gen/gen_ai_hub/orchestration/models/template.py b/packages/gen/gen_ai_hub/orchestration/models/template.py new file mode 100644 index 0000000..5b9aa4d --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/template.py @@ -0,0 +1,95 @@ +from typing import List, Optional, Union, NamedTuple, Dict, Any + +from gen_ai_hub.orchestration.models.base import JSONSerializable +from gen_ai_hub.orchestration.models.message import Message +from gen_ai_hub.orchestration.models.response_format import ( + ResponseFormatType, + ResponseFormatJsonSchema, + ResponseFormatFactory +) +from gen_ai_hub.orchestration.models.tools import ChatCompletionTool + + +class TemplateValue(NamedTuple): + """ + Represents a named value for use in template substitution. + + This class pairs a name with a corresponding value, which can be a string, + integer, or float. It's designed to be used in template rendering processes + where named placeholders are replaced with specific values. + """ + + name: str + """The identifier for this template value.""" + value: Union[str, int, float] + """The actual value to be used in substitution.""" + +class Template(JSONSerializable): + """ + Represents a configurable template for generating prompts or conversations. + """ + + def __init__( + self, + messages: List[Message], + defaults: Optional[List[TemplateValue]] = None, + tools: Optional[List[Union[dict, ChatCompletionTool]]] = None, + response_format: Optional[Union[ + ResponseFormatType.TEXT, + ResponseFormatType.JSON_OBJECT, + ResponseFormatJsonSchema + ]] = None, + ): + """Initializes a Template instance. + + :param messages: list of prompt messages that form the template._ + :type messages: List[Message] + :param defaults: list of default values for template variables, defaults to None + :type defaults: Optional[List[TemplateValue]], optional + :param tools: list of tool definitions, defaults to None + :type tools: Optional[List[Union[dict, ChatCompletionTool]]], optional + :param response_format: response format that the model output should adhere to, defaults to None + :type response_format: Optional[Union[ ResponseFormatType.TEXT, + ResponseFormatType.JSON_OBJECT, ResponseFormatJsonSchema ]], optional + """ + self.messages = messages + self.defaults = defaults or [] + self.tools = tools or [] + self.response_format = response_format + + def to_dict(self) -> Dict[str, Any]: + """Converts the Template instance to a dictionary representation. + Serializes the template to a dictionary, converting tools as needed. + + :raises ValueError: If an invalid tool is encountered in the tools list. + :return: A dictionary representation of the Template instance. + :rtype: Dict[str, Any] + """ + + template_dict: Dict[str, Any] = { + "template": [message.to_dict() for message in self.messages], + "defaults": {default.name: default.value for default in self.defaults}, + } + + if self.tools: + tool_dicts = [] + for idx, tool in enumerate(self.tools): + if isinstance(tool, ChatCompletionTool): + tool_dicts.append(tool.to_dict()) + elif isinstance(tool, dict): + tool_dicts.append(tool) + else: + raise ValueError( + f"Invalid tool at index {idx}: {tool!r} (type: {type(tool).__name__}). " + "If you are passing a function, decorate it with @function_tool." + ) + template_dict["tools"] = tool_dicts + + if self.response_format: + template_dict["response_format"] = ( + ResponseFormatFactory.create_response_format_object( + self.response_format + ).to_dict() + ) + + return template_dict diff --git a/packages/gen/gen_ai_hub/orchestration/models/template_ref.py b/packages/gen/gen_ai_hub/orchestration/models/template_ref.py new file mode 100644 index 0000000..957fb8b --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/template_ref.py @@ -0,0 +1,61 @@ +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +class TemplateRef(JSONSerializable): + """ + Represents a prompt template reference for generating prompts or conversations. + + This is a factory class for creating a reference to a prompt template. + It is used to reference a template by id, or the tuple: scenario, name, version + + """ + + def __init__(self, **kwargs): + """Initializes a TemplateRef instance with dynamic attributes.""" + for key, value in kwargs.items(): + setattr(self, key, value) + + @classmethod + def from_id( + cls, + prompt_template_id: str + ): + """Creates a TemplateRef instance from a prompt template ID. + + :param prompt_template_id: The ID of the prompt template. + :type prompt_template_id: str + :return: A TemplateRef instance with the specified ID. + :rtype: TemplateRef + """ + return cls(id=prompt_template_id) + + @classmethod + def from_tuple( + cls, + scenario: str, + name: str, + version: str + ): + """Creates a TemplateRef instance from a scenario, name, and version. + + :param scenario: The scenario of the prompt template. + :type scenario: str + :param name: The name of the prompt template. + :type name: str + :param version: The version of the prompt template. + :type version: str + :return: A TemplateRef instance with the specified scenario, name, and version. + :rtype: TemplateRef + """ + return cls(scenario=scenario, name=name, version=version) + + def to_dict(self): + """Converts the TemplateRef instance to a dictionary representation. + + :return: A dictionary representation of the TemplateRef instance. + :rtype: dict + """ + template_ref = {} + for key, value in self.__dict__.items(): + template_ref[key] = value + return { "template_ref": template_ref } diff --git a/packages/gen/gen_ai_hub/orchestration/models/tools.py b/packages/gen/gen_ai_hub/orchestration/models/tools.py new file mode 100644 index 0000000..f6c7c3b --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/tools.py @@ -0,0 +1,261 @@ +import inspect +import typing +from typing import Any, Callable, Dict, Optional + +from gen_ai_hub.orchestration.models.base import JSONSerializable + + +def python_type_to_json_type(py_type): + """Converts a Python type to a JSON Schema type. + + :param py_type: The Python type to convert. + :type py_type: type + :return: A dictionary representing the JSON Schema type. + :rtype: Dict[str, Any] + """ + origin = typing.get_origin(py_type) + args = typing.get_args(py_type) + + # Simple types + if py_type is str: + return {"type": "string"} + if py_type in (int, float): + return {"type": "number"} + if py_type is bool: + return {"type": "boolean"} + if py_type is type(None): + return {"type": "null"} + + # List/array + if origin in (list, typing.List): + item_type = args[0] if args else str + return { + "type": "array", + "items": python_type_to_json_type(item_type) + } + + # Dict/object + if origin in (dict, typing.Dict): + value_type = args[1] if len(args) > 1 else str + return { + "type": "object", + "additionalProperties": python_type_to_json_type(value_type) + } + + # Union/Optional + if origin is typing.Union: + json_types = [python_type_to_json_type(a) for a in args] + # Handle Optional[X] (Union[X, NoneType]) + non_null_types = [t for t in json_types if t.get("type") != "null"] + if len(json_types) == 2 and len(non_null_types) == 1: + result = non_null_types[0].copy() + result["nullable"] = True + return result + return {"anyOf": json_types} + + # Fallback + return {"type": "string"} + +class ChatCompletionTool(JSONSerializable): + """ + Base class for all chat completion tools. + """ + def __init__(self, type_: str): + """Initializes a ChatCompletionTool instance. + + :param type_: The type of the tool. + :type type_: str + """ + self.type = type_ + + def to_dict(self) -> Dict[str, Any]: + """Converts the ChatCompletionTool instance to a dictionary representation. + + :return: A dictionary representation of the ChatCompletionTool instance. + :rtype: Dict[str, Any] + """ + return { + "type": self.type, + } + + +class FunctionTool(ChatCompletionTool): + """ + Represents a function tool for OpenAI-like function calling. + """ + def __init__( + self, + name: str, + parameters: dict, + strict: bool = False, + description: Optional[str] = None, + function: Optional[Callable] = None, + ): + """Initializes a FunctionTool instance. + + :param name: The name of the function. + :type name: str + :param parameters: The parameters schema for the function. + :type parameters: dict + :param strict: Whether to enforce strict parameter checking, defaults to False + :type strict: bool, optional + :param description: The description of the function, defaults to None + :type description: Optional[str], optional + :param function: The actual callable function, defaults to None + :type function: Optional[Callable], optional + """ + super().__init__(type_="function") + self.name = name + self.description = description + self.parameters = parameters + self.strict = strict + self.function = function + + def to_dict(self) -> Dict[str, Any]: + """Converts the FunctionTool instance to a dictionary representation. + + :return: A dictionary representation of the FunctionTool instance. + :rtype: Dict[str, Any] + """ + base = { + "type": self.type, + "function": { + "name": self.name, + "parameters": self.parameters, + "strict": self.strict, + }, + } + + if self.description: + base["function"]["description"] = self.description + + return base + + def execute(self, **kwargs: Any) -> Any: + """Execute the function with the provided arguments. + + :raises ValueError: If the function is not set or if unexpected arguments are provided in strict mode. + :return: The result of the function execution. + :rtype: Any + """ + if self.function is None: + raise ValueError("Function is not set.") + + if self.strict: + for key in kwargs.keys(): + if key not in self.parameters["properties"]: + raise ValueError(f"Unexpected argument '{key}' for function '{self.name}'") + + return self.function(**kwargs) + + async def aexecute(self, **kwargs: Any) -> Any: + """Asynchronously execute the function with the provided arguments. + + :raises ValueError: If the function is not set or if unexpected arguments are provided in strict mode. + :return: The result of the function execution. + :rtype: Any + """ + if self.function is None: + raise ValueError("Function is not set.") + + if self.strict: + for key in kwargs.keys(): + if key not in self.parameters["properties"]: + raise ValueError(f"Unexpected argument '{key}' for function '{self.name}'") + + return await self.function(**kwargs) + + + @staticmethod + def from_function( + func: Callable, + *, + description: Optional[str] = None, + strict: bool = False + ) -> "FunctionTool": + """Create a FunctionTool from a Python function. + + :param func: The Python function to convert. + :type func: Callable + :param description: The description of the function, defaults to None + :type description: Optional[str], optional + :param strict: Whether to enforce strict parameter checking, defaults to False + :type strict: bool, optional + :raises TypeError: If any parameter is missing a type hint. + :return: A FunctionTool instance. + :rtype: FunctionTool + """ + + tool_description = description or inspect.getdoc(func) + sig = inspect.signature(func) + type_hints = typing.get_type_hints(func) + param_schema = {} + + for name, param in sig.parameters.items(): + if name not in type_hints: + raise TypeError( + f"Parameter '{name}' in '{func.__name__}' is missing a type hint." + ) + param_type = type_hints.get(name, str) + param_schema[name] = python_type_to_json_type(param_type) + + parameters = { + "type": "object", + "properties": param_schema, + "required": [ + name for name, param in sig.parameters.items() + if param.default is inspect.Parameter.empty + ], + "additionalProperties": False + } + + return FunctionTool( + name=func.__name__, + description=tool_description, + parameters=parameters, + strict=strict, + function=func, + ) + + +def function_tool( + func: Optional[Callable] = None, *, description: Optional[str] = None, strict: bool = False +) -> Callable[[Callable], FunctionTool] | FunctionTool: + """Create a decorator that converts a function into a FunctionTool. + + Usage: + + @function_tool + + def my_func(...): ... + + @function_tool() + + def my_func(...): ... + + :param func: The function to convert, defaults to None + :type func: Optional[Callable], optional + :param description: The description of the function, defaults to None + :type description: Optional[str], optional + :param strict: Whether to enforce strict parameter checking, defaults to False + :type strict: bool, optional + :return: A FunctionTool instance or a decorator function. + :rtype: Callable[[Callable], FunctionTool] | FunctionTool + """ + + def decorator(func_: Callable) -> FunctionTool: + """Create a FunctionTool from the decorated function. + + :param func_: The decorated function. + :type func_: Callable + :return: A FunctionTool instance. + :rtype: FunctionTool + """ + return FunctionTool.from_function(func=func_, description=description, strict=strict) + + if func is not None and callable(func): + # Used as @function_tool + return decorator(func) + else: + # Used as @function_tool() + return decorator diff --git a/packages/gen/gen_ai_hub/orchestration/models/translation/sap_document_translation.py b/packages/gen/gen_ai_hub/orchestration/models/translation/sap_document_translation.py new file mode 100644 index 0000000..d63d44f --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/translation/sap_document_translation.py @@ -0,0 +1,34 @@ +from gen_ai_hub.orchestration.models.translation.translation import Translation +from gen_ai_hub.orchestration.models.translation.translation import InputTranslationModule, \ + InputTranslationConfig, OutputTranslationModule, OutputTranslationConfig, TranslationType + + +class SAPDocumentTranslation(Translation): + """SAPTranslationHub represents the translation service provided by SAP.""" + + def __init__(self, input_translation_config: InputTranslationConfig = None, + output_translation_config: OutputTranslationConfig = None): + """Initializes the SAPDocumentTranslation with optional input and output translation configurations. + + :param input_translation_config: the configuration for input translation, defaults to None + :type input_translation_config: InputTranslationConfig, optional + :param output_translation_config: the configuration for output translation, defaults to None + :type output_translation_config: OutputTranslationConfig, optional + """ + + input_translation_module = None + output_translation_module = None + + if input_translation_config is not None: + input_translation_module = InputTranslationModule( + type=TranslationType.SAP_DOCUMENT_TRANSLATION, + config=input_translation_config + ) + + if output_translation_config is not None: + output_translation_module = OutputTranslationModule( + type=TranslationType.SAP_DOCUMENT_TRANSLATION, + config=output_translation_config + ) + super().__init__(input_translation=input_translation_module, + output_translation=output_translation_module) diff --git a/packages/gen/gen_ai_hub/orchestration/models/translation/translation.py b/packages/gen/gen_ai_hub/orchestration/models/translation/translation.py new file mode 100644 index 0000000..4a1f6dc --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/models/translation/translation.py @@ -0,0 +1,143 @@ +from gen_ai_hub.orchestration.models.base import JSONSerializable +from enum import Enum + + +class TranslationType(str, Enum): + """Enumerates supported translation types.""" + SAP_DOCUMENT_TRANSLATION = "sap_document_translation" + + +class InputTranslationConfig(JSONSerializable): + """Configuration for input translation. These parameters are specific to SAP Translation Hub.""" + + def __init__(self, source_language: str, target_language: str): + """Initializes the InputTranslationConfig with source and target languages. + + :param source_language: the source language code (e.g., 'de-DE' for German). + :type source_language: str + :param target_language: the target language code (e.g., 'en-US' for US English). + :type target_language: str + """ + + self.source_language = source_language + self.target_language = target_language + + def to_dict(self): + """to_dict method to convert the configuration to a dictionary. + + :return: dictionary representation of the configuration. + :rtype: dict + """ + + return { + "source_language": self.source_language, + "target_language": self.target_language + } + + +class InputTranslationModule(JSONSerializable): + """Configuration for input translation module. + + :param JSONSerializable: _description_ + :type JSONSerializable: _type_ + :return: _description_ + :rtype: _type_ + """ + + def __init__(self, type: str, config: InputTranslationConfig): + """Initializes the InputTranslationModule with type and configuration. + + :param type: The type of translation module (e.g., 'sap_document_translation'). + :type type: str + :param config: Configuration object for the translation module. + :type config: InputTranslationConfig + """ + + self.type = type + self.config = config + + def to_dict(self): + """to_dict method to convert the module to a dictionary. + + :return: dictionary representation of the module. + :rtype: dict + """ + return { + "type": self.type, + "config": self.config.to_dict() + } + + +class OutputTranslationConfig(JSONSerializable): + """Configuration for output translation. + + :param JSONSerializable: _description_ + :type JSONSerializable: _type_ + :return: _description_ + :rtype: _type_ + """ + + def __init__(self, target_language: str, source_language: str = None): + """Initializes the OutputTranslationConfig with target and optional source languages. These parameters are specific to SAP Translation Hub. + + :param target_language: the target language code (e.g., 'en-US' for US English). + :type target_language: str + :param source_language: the source language code (e.g., 'de-DE' for German), defaults to None + :type source_language: str, optional + """ + self.target_language = target_language + self.source_language = source_language + + def to_dict(self): + """to_dict method to convert the configuration to a dictionary. + :return: dictionary representation of the configuration. + :rtype: dict + """ + + return { + "target_language": self.target_language, + "source_language": self.source_language + } + + +class OutputTranslationModule(JSONSerializable): + """Configuration for output translation module.""" + + def __init__(self, type: str, config: OutputTranslationConfig): + """Initializes the OutputTranslationModule with type and configuration. + + :param type: The type of translation module (e.g., 'sap_document_translation'). + :type type: str + :param config: Configuration object for the translation module. + :type config: OutputTranslationConfig + """ + + self.type = type + self.config = config + + def to_dict(self): + """to_dict method to convert the module to a dictionary. + :return: dictionary representation of the module. + :rtype: dict + """ + + return { + "type": self.type, + "config": self.config.to_dict() + } + + +class Translation: + """Translation module for managing input and output translations.""" + + def __init__(self, input_translation: InputTranslationModule = None, + output_translation: OutputTranslationModule = None): + """Initializes the Translation module with optional input and output translation configurations. + + :param input_translation: the configuration for input translation, defaults to None + :type input_translation: InputTranslationModule, optional + :param output_translation: the configuration for output translation, defaults to None + :type output_translation: OutputTranslationModule, optional + """ + self.input_translation = input_translation + self.output_translation = output_translation diff --git a/packages/gen/gen_ai_hub/orchestration/service.py b/packages/gen/gen_ai_hub/orchestration/service.py new file mode 100644 index 0000000..349abd2 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/service.py @@ -0,0 +1,708 @@ +""" +Module for orchestration service handling requests and responses. + +Provides synchronous and asynchronous methods to run orchestration pipelines. +""" + +from copy import deepcopy +from dataclasses import dataclass +from enum import Enum +from functools import wraps +import asyncio +import random +import time +import logging +from typing import List, Optional, Iterable, Union + +import dacite +from gen_ai_hub.orchestration.exceptions import OrchestrationError +import httpx +from ai_api_client_sdk.models.status import Status + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.orchestration.models.base import JSONSerializable +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.message import Message +from gen_ai_hub.orchestration.models.response import OrchestrationResponse, OrchestrationResponseStreaming, \ + OrchestrationResponseWithRetries +from gen_ai_hub.orchestration.models.template import TemplateValue +from gen_ai_hub.orchestration.sse_client import SSEClient, AsyncSSEClient, _handle_http_error +from gen_ai_hub.proxy import get_proxy_client + +COMPLETION_SUFFIX = "/completion" + + +@dataclass +class OrchestrationRequest(JSONSerializable): + """ + Represents a request for the orchestration process, including configuration, + template values, and message history. + """ + config: OrchestrationConfig + """The orchestration configuration for the request. + + :return: OrchestrationConfig + :rtype: OrchestrationConfig + """ + template_values: List[TemplateValue] + """List of template values to be used in the orchestration.""" + history: List[Message] + """List of messages representing the conversation history.""" + + def to_dict(self): + """Converts the OrchestrationRequest instance to a dictionary. + + :return: Dictionary representation of the OrchestrationRequest + :rtype: dict + """ + return { + "orchestration_config": self.config.to_dict(), + "input_params": {value.name: str(value.value) for value in self.template_values}, + "messages_history": [message.to_dict() for message in self.history], + } + + +def cache_if_not_none(func): + """Custom cache decorator that only caches non-None results + + :param func: The function to be decorated. + :type func: callable + :return: The decorated function with caching behavior. + :rtype: callable + """ + cache = {} + + @wraps(func) + def wrapper(*args, **kwargs): + """Wrapper function that implements caching logic. + + :return: The result of the decorated function, either from cache or freshly computed. + :rtype: Any + """ + key = (args, frozenset(kwargs.items())) # Create hashable key for cache + if key not in cache: + result = func(*args, **kwargs) + if result is not None: # Only cache if result is not None + cache[key] = result + return result + return cache[key] + + def cache_clear(): + cache.clear() + + wrapper.cache_clear = cache_clear + return wrapper + + +# pylint: disable=too-many-arguments,too-many-positional-arguments +@cache_if_not_none +def discover_orchestration_api_url(base_url: str, + auth_url: str, + client_id: str, + client_secret: str, + resource_group: str, + config_id: Optional[str] = None, + config_name: Optional[str] = None, + orchestration_scenario: str = "orchestration", + executable_id: str = "orchestration") -> Optional[str]: + """Discovers the orchestration API URL based on provided configuration details. + + :param base_url: the base URL for the AI Core API. + :type base_url: str + :param auth_url: the URL for the AI Core authentication service. + :type auth_url: str + :param client_id: the client ID for the AI Core API. + :type client_id: str + :param client_secret: the client secret for the AI Core API. + :type client_secret: str + :param resource_group: the resource group for the AI Core API. + :type resource_group: str + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param orchestration_scenario: the orchestration scenario ID, defaults to "orchestration" + :type orchestration_scenario: str, optional + :param executable_id: the orchestration executable ID, defaults to "orchestration" + :type executable_id: str, optional + :return: The orchestration API URL or None if no deployment is found. + :rtype: Optional[str] + """ + proxy_client = GenAIHubProxyClient( + base_url=base_url, + auth_url=auth_url, + client_id=client_id, + client_secret=client_secret, + resource_group=resource_group + ) + deployments = proxy_client.ai_core_client.deployment.query( + scenario_id=orchestration_scenario, + executable_ids=[executable_id], + status=Status.RUNNING + ) + if deployments.count > 0: + sorted_deployments = sorted(deployments.resources, key=lambda x: x.start_time)[::-1] + check_for = {} + if config_name: + check_for["configuration_name"] = config_name + if config_id: + check_for["configuration_id"] = config_id + if not check_for: + return sorted_deployments[0].deployment_url + for deployment in sorted_deployments: + if all(getattr(deployment, key) == value for key, value in check_for.items()): + return deployment.deployment_url + return None + + +def get_orchestration_api_url(proxy_client: GenAIHubProxyClient, + deployment_id: Optional[str] = None, + config_name: Optional[str] = None, + config_id: Optional[str] = None) -> str: + """Retrieves the orchestration API URL based on provided deployment or configuration details. + + :param proxy_client: The GenAIHubProxyClient instance. + :type proxy_client: GenAIHubProxyClient + :param deployment_id: the deployment ID, defaults to None + :type deployment_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :raises ValueError: If no orchestration deployment is found. + :return: The orchestration API URL. + :rtype: str + """ + + if deployment_id: + return f"{proxy_client.ai_core_client.base_url.rstrip('/')}/inference/deployments/{deployment_id}" + url = discover_orchestration_api_url( + **proxy_client.model_dump(exclude='ai_core_client'), + config_name=config_name, + config_id=config_id + ) + if url is None: + raise ValueError('No Orchestration deployment found!') + return url + + +class OrchestrationService: + """A service for executing orchestration requests, allowing for the generation of + LLM-generated content through a pipeline of configured modules. This service supports both synchronous and + asynchronous request execution. For streaming responses, special care is taken to not close the underlying + HTTP stream prematurely. + + https://api.sap.com/api/ORCHESTRATION_API/overview + """ + + def __init__(self, + api_url: Optional[str] = None, + config: Optional[OrchestrationConfig] = None, + proxy_client: Optional[GenAIHubProxyClient] = None, + deployment_id: Optional[str] = None, + config_name: Optional[str] = None, + config_id: Optional[str] = None, + timeout: Union[int, float, httpx.Timeout, None] = None): + """Initializes the OrchestrationService with the provided parameters. + + :param api_url: The base URL for the orchestration API, defaults to None + :type api_url: Optional[str], optional + :param config: The default orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param proxy_client: The GenAIHubProxyClient instance, defaults to None + :type proxy_client: Optional[GenAIHubProxyClient], optional + :param deployment_id: the deployment ID, defaults to None + :type deployment_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :param timeout: the timeout for HTTP requests, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + """ + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + if api_url: + self.api_url = api_url + else: + self.api_url = get_orchestration_api_url(self.proxy_client, deployment_id, config_name, config_id) + self.config = config + self.timeout = timeout + # create reusable httpx client to improve performance + self.client = httpx.Client(timeout=self.timeout) + self.async_client = httpx.AsyncClient(timeout=self.timeout) + + def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + # Determine the timeout to use for this request + if timeout is not None: + # Overwrite default timeout for this request + request_timeout = timeout + elif self.timeout is not None: + # Use the default timeout is set + request_timeout = self.timeout + else: + # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) + request_timeout = httpx.USE_CLIENT_DEFAULT + return request_timeout + + def _should_retry(self, error: Exception) -> bool: + """Determines if a request should be retried based on the error type. + + :param error: The exception that occurred. + :type error: Exception + :return: True if the error is retryable (only 429 rate limit errors), False otherwise. + :rtype: bool + """ + if isinstance(error, httpx.HTTPStatusError): + return error.response.status_code == 429 + return False + + def _get_retry_after(self, error: Exception) -> Optional[float]: + """Extracts the Retry-After header value from a 429 response if available. + + :param error: The exception that occurred. + :type error: Exception + :return: Number of seconds to wait before retrying, or None if not specified. + :rtype: Optional[float] + """ + if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 429: + retry_after = error.response.headers.get('Retry-After') + if retry_after: + try: + # Retry-After can be in seconds (integer) or HTTP date format + return float(retry_after) + except ValueError: + # If it's a date format, we'll fall back to exponential backoff + return None + return None + + def _calculate_backoff(self, retry_count: int, base_delay: float = 1.0, max_delay: float = 60.0, + min_delay: float = 0.0) -> float: + """Calculates exponential backoff delay with jitter. + + :param retry_count: The current retry attempt number. + :type retry_count: int + :param base_delay: the initial delay in seconds, defaults to 1.0 + :type base_delay: float, optional + :param max_delay: the maximum delay in seconds, defaults to 60.0 + :type max_delay: float, optional + :param min_delay: the minimum delay in seconds, defaults to 0.0 + :type min_delay: float, optional + :return: Delay in seconds before the next retry. + :rtype: float + """ + # Calculate exponential delay: base_delay * 2^retry_count + exp_delay = base_delay * (2 ** retry_count) + + # Cap at max_delay + capped = min(exp_delay, max_delay) + + # Ensure the lower bound doesn't exceed the cap + lower = max(0.0, min_delay) + if lower >= capped: + return capped + + # Return random value in range [lower, capped] for jitter + return random.uniform(lower, capped) + + def _execute_request( + self, + config: OrchestrationConfig, + template_values: List[TemplateValue], + history: List[Message], + stream: bool, + stream_options: Optional[dict] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> Union[OrchestrationResponse, Iterable[OrchestrationResponseStreaming]]: + """Executes an orchestration request synchronously. + For streaming requests, this method creates a single HTTP stream. It manually enters the stream's + context to obtain the response, checks for HTTP errors, and then passes both the open response and + a custom close function to the SSE client. The SSEClient will then yield streaming events and + close the HTTP stream upon completion. + + :param config: the orchestration configuration. + :type config: OrchestrationConfig + :param template_values: the template values for the request. + :type template_values: List[TemplateValue] + :param history: the message history. + :type history: List[Message] + :param stream: whether to stream the response. + :type stream: bool + :param stream_options: additional streaming options, defaults to None + :type stream_options: Optional[dict], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :raises ValueError: If no configuration is provided. + :raises OrchestrationError: If the HTTP request fails. + :return: An OrchestrationResponse if not streaming, or an iterable of OrchestrationResponseStreaming + :rtype: Union[OrchestrationResponse, Iterable[OrchestrationResponseStreaming]] + :yield: OrchestrationResponseStreaming objects if streaming. + :rtype: Iterator[Union[OrchestrationResponse, Iterable[OrchestrationResponseStreaming]]] + """ + if config is None: + raise ValueError("A configuration is required to invoke the orchestration service.") + config_copy = deepcopy(config) + config_copy._stream = stream + if stream_options: + config_copy.stream_options = stream_options + request_obj = OrchestrationRequest( + config=config_copy, + template_values=template_values or [], + history=history or [], + ) + + if stream: + # Create the streaming response context manager. + response_cm = self.client.stream( + "POST", + self.api_url + COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.to_dict(), + timeout=self._determine_timeout(timeout) + ) + return SSEClient(response_cm, prefix="data: ", final_message="[DONE]") + + response = self.client.post( + self.api_url + COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.to_dict(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return dacite.from_dict( + data_class=OrchestrationResponse, + data=data, + config=dacite.Config(cast=[Enum]), + ) + + async def _a_execute_request( + self, + config: OrchestrationConfig, + template_values: List[TemplateValue], + history: List[Message], + stream: bool, + stream_options: Optional[dict] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> Union[OrchestrationResponse, AsyncSSEClient]: + """Executes an orchestration request asynchronously. + + :param config: the orchestration configuration. + :type config: OrchestrationConfig + :param template_values: the template values for the request. + :type template_values: List[TemplateValue] + :param history: the message history. + :type history: List[Message] + :param stream: whether to stream the response. + :type stream: bool + :param stream_options: additional streaming options, defaults to None + :type stream_options: Optional[dict], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :raises ValueError: If no configuration is provided. + :raises OrchestrationError: If the HTTP request fails. + :return: An OrchestrationResponse if not streaming, or an AsyncSSEClient for iterating over + the streaming response. + :rtype: Union[OrchestrationResponse, AsyncSSEClient] + """ + if config is None: + raise ValueError("A configuration is required to invoke the orchestration service.") + config_copy = deepcopy(config) + config_copy._stream = stream + if stream_options: + config_copy.stream_options = stream_options + request_obj = OrchestrationRequest( + config=config_copy, + template_values=template_values or [], + history=history or [], + ) + + if stream: + response_cm = self.async_client.stream( + "POST", + self.api_url + COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.to_dict(), + timeout=self._determine_timeout(timeout) + ) + return AsyncSSEClient(response_cm, prefix="data: ", final_message="[DONE]") + + response = await self.async_client.post( + self.api_url + COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.to_dict(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return dacite.from_dict( + data_class=OrchestrationResponse, + data=data, + config=dacite.Config(cast=[Enum]), + ) + + def run( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> OrchestrationResponse: + """Executes an orchestration request synchronously (non-streaming). + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: An OrchestrationResponse object. + :rtype: OrchestrationResponse + """ + return self._execute_request( + config=config or self.config, + template_values=template_values, + history=history, + stream=False, + timeout=timeout, + ) + + def stream( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + stream_options: Optional[dict] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> SSEClient: + """Executes an orchestration request in streaming mode (synchronously). + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param stream_options: the additional streaming options, defaults to None + :type stream_options: Optional[dict], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: An SSEClient instance for iterating over the streaming response. + :rtype: SSEClient + """ + return self._execute_request( + config=config or self.config, + template_values=template_values, + history=history, + stream=True, + stream_options=stream_options, + timeout=timeout, + ) + + async def arun( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> OrchestrationResponse: + """Executes an orchestration request asynchronously (non-streaming). + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: An OrchestrationResponse object. + :rtype: OrchestrationResponse + """ + return await self._a_execute_request( + config=config or self.config, + template_values=template_values, + history=history, + stream=False, + timeout=timeout, + ) + + async def astream( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + stream_options: Optional[dict] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> AsyncSSEClient: + """Executes an orchestration request asynchronously in streaming mode. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param stream_options: the additional streaming options, defaults to None + :type stream_options: Optional[dict], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: An AsyncSSEClient instance for iterating over the streaming response. + :rtype: AsyncSSEClient + """ + return await self._a_execute_request( + config=config or self.config, + template_values=template_values, + history=history, + stream=True, + stream_options=stream_options, + timeout=timeout, + ) + + def close_http_connection(self): + """ + Closes the httpx synchronous client. + """ + self.client.close() + + async def aclose_http_connection(self): + """ + Closes the httpx asynchronous client. + """ + await self.async_client.aclose() + + def run_with_retries( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + max_retries: int = 10, + base_delay: float = 1.0, + ) -> OrchestrationResponseWithRetries | None: + """Executes an orchestration request with automatic retry on rate limits (429) and server errors. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :param max_retries: the maximum number of retry attempts, defaults to 10 + :type max_retries: int, optional + :param base_delay: the initial delay between retries in seconds, defaults to 1.0 + :type base_delay: float, optional + :return: An OrchestrationResponseWithRetries with retry count information. + :rtype: OrchestrationResponseWithRetries | None + :raises OrchestrationError: If the request fails after all retries (includes retry count). + :raises ValueError: If no configuration is provided. + """ + for retry_count in range(max_retries + 1): + try: + # Execute the request + response = self.run( + config=config, + template_values=template_values, + history=history, + timeout=timeout, + ) + + # Success, response with retry count + return OrchestrationResponseWithRetries( + request_id=response.request_id, + module_results=response.module_results, + orchestration_result=response.orchestration_result, + retries=retry_count, + ) + + except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + time.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) + return None + + def handle_retry(self, retry_count: int, base_delay: float, error: OrchestrationError, max_retries: int) -> float: + """Handles retry logic with exponential backoff and jitter. + If Retry-After header exists, use it as min_delay to add jitter on top + + :param retry_count: the current retry attempt number + :type retry_count: int + :param base_delay: the initial delay between retries in seconds + :type base_delay: float + :param error: the exception that occurred + :type error: OrchestrationError + :param max_retries: the maximum number of retry attempts + :type max_retries: int + :raises error: Raises the original error if no more retries should be attempted + :return: number of seconds to wait before next retry + :rtype: float + """ + if not self._should_retry(error) or retry_count >= max_retries: + error.retries = retry_count + raise error + + retry_after = self._get_retry_after(error) + delay = self._calculate_backoff(retry_count, base_delay, + min_delay=0.0 if retry_after is None else retry_after) + logging.info("Retry no. %d, due to rate limiting", retry_count) + return delay + + async def arun_with_retries( + self, + config: Optional[OrchestrationConfig] = None, + template_values: Optional[List[TemplateValue]] = None, + history: Optional[List[Message]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + max_retries: int = 10, + base_delay: float = 1.0, + ) -> OrchestrationResponseWithRetries | None: + """Executes an orchestration request asynchronously with automatic retry on rate limits (429) and + server errors. Uses exponential backoff with jitter to handle rate limiting gracefully. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param template_values: the template values for the request, defaults to None + :type template_values: Optional[List[TemplateValue]], optional + :param history: the message history, defaults to None + :type history: Optional[List[Message]], optional + :param timeout: the timeout for the request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :param max_retries: the maximum number of retry attempts, defaults to 10 + :type max_retries: int, optional + :param base_delay: the initial delay between retries in seconds, defaults to 1.0 + :type base_delay: float, optional + :return: An OrchestrationResponseWithRetries with retry count information. + :rtype: OrchestrationResponseWithRetries | None + :raises OrchestrationError: If the request fails after all retries (includes retry count). + :raises ValueError: If no configuration is provided. + """ + for retry_count in range(max_retries + 1): + try: + # Execute the request + response = await self.arun( + config=config, + template_values=template_values, + history=history, + timeout=timeout, + ) + + # Success! Return response with retry count + return OrchestrationResponseWithRetries( + request_id=response.request_id, + module_results=response.module_results, + orchestration_result=response.orchestration_result, + retries=retry_count, + ) + + except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + await asyncio.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) + return None diff --git a/packages/gen/gen_ai_hub/orchestration/sse_client.py b/packages/gen/gen_ai_hub/orchestration/sse_client.py new file mode 100644 index 0000000..26cb9a8 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/sse_client.py @@ -0,0 +1,302 @@ +""" +Module for Server-Sent Events (SSE) clients for orchestration responses. + +This module provides both synchronous and asynchronous SSE clients for iterating over streaming responses. +Each client is responsible for handling HTTP errors and for closing the underlying HTTP stream +when iteration is complete. +""" + +import json +from enum import Enum +from typing import Iterable, Iterator, AsyncIterator + +import dacite +import httpx + +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.response import OrchestrationResponseStreaming + + +def _parse_event_data(event_data: str, final_message: str) -> "OrchestrationResponseStreaming": + """Parses event data from a JSON string into an OrchestrationResponseStreaming object. + + :param event_data: the JSON string containing event data. + :type event_data: str + :param final_message: a message indicating the end of the stream. + :type final_message: str + :raises OrchestrationError: if the event data contains an error code. + :return: An OrchestrationResponseStreaming object parsed from the event data. + :rtype: OrchestrationResponseStreaming + """ + if event_data == final_message: + return None + event = json.loads(event_data) + if "code" in event: + raise OrchestrationError( + request_id=event.get("request_id"), + http_headers=httpx.Headers({}), + message=event.get("message"), + code=event.get("code"), + location=event.get("location"), + module_results=event.get("module_results", {}), + ) + return dacite.from_dict( + data=event, + data_class=OrchestrationResponseStreaming, + config=dacite.Config(cast=[Enum]), + ) + + +class SSEClient: + """ + A synchronous Server-Sent Events (SSE) client that wraps an httpx.Response for iterating + over streaming responses. + + This client reads data chunks from the HTTP stream and parses each SSE event. + For performance reasons the underlying HTTP stream is reused for subsequent calls. + """ + + def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): + """Initializes the SSEClient. + + :param response_cm: An httpx.Response context manager for the streaming response. + :type response_cm: httpx.Response + :param prefix: The prefix string that identifies SSE event data, defaults to "data: " + :type prefix: str, optional + :param final_message: The message that indicates the end of the stream, defaults to "[DONE]" + :type final_message: str, optional + """ + self.response_cm = response_cm + self.event_prefix = prefix + self.final_message = final_message + self._response = None + self._iterator = None + + + def __enter__(self): + """ + Synchronously enters the context for the streaming response. + + It awaits the response, checks for HTTP errors, and if an error occurs, + reads the content and raises an OrchestrationError. + + return: Self, with the streaming response stored. + rtype: SSEClient + """ + self._response = self.response_cm.__enter__() + try: + self._response.raise_for_status() + except httpx.HTTPStatusError as error: + content = self._response.read() + error_response = httpx.Response( + status_code=self._response.status_code, + headers=self._response.headers, + content=content, + request=self._response.request, + ) + self.response_cm.__exit__(None, None, None) + _handle_http_error(error, error_response) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Synchronously exits the context, ensuring that the context manager is properly closed. + """ + self.response_cm.__exit__(exc_type, exc_val, exc_tb) + + def iter_lines(self) -> Iterable[str]: + """ + Reads data chunks from the HTTP stream and yields complete lines. + + This method accumulates incoming chunks until a newline is encountered, yielding one complete + line at a time. + + yield: Complete lines of text from the streaming response. + """ + buffer = "" + for chunk in self._response.iter_text(): + buffer += chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + yield line.strip() + if buffer: + yield buffer.strip() + + def __iter__(self) -> Iterator: + """ + Returns self as an iterator. Opens the HTTP stream and initializes the internal iterator. + """ + return self + + def __next__(self): + """ + Retrieves the next parsed SSE event from the stream. + It skips any lines that do not start with the expected prefix. When the final message is encountered + or the stream is exhausted, it closes the stream and raises StopIteration. + """ + if self._iterator is None: + self.__enter__() + self._iterator = self.iter_lines() + while True: + try: + line = next(self._iterator) + except StopIteration: + # End of stream; ensure resources are cleaned up. + self.__exit__(None, None, None) + raise StopIteration + + if not line or not line.startswith(self.event_prefix): + continue + + event_data = line[len(self.event_prefix):] + result = _parse_event_data(event_data, self.final_message) + if result is None: + # Final message encountered; close the stream. + self.__exit__(None, None, None) + raise StopIteration + return result + + +class AsyncSSEClient: + """ + An asynchronous SSE client for iterating over streaming responses. + + This client wraps an asynchronous HTTP stream (provided as a context manager) and ensures + that the stream is properly opened and closed. It also checks for HTTP errors upon entering the stream. + """ + + def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): + """Initializes the AsyncSSEClient. + + :param response_cm: An asynchronous context manager for the HTTP streaming response. + :type response_cm: the type of an async context manager returning httpx.Response + :param prefix: The SSE data prefix, defaults to "data: " + :type prefix: str, optional + :param final_message: The message indicating the end of the stream, defaults to "[DONE]" + :type final_message: str, optional + """ + self.response_cm = response_cm + self.event_prefix = prefix + self.final_message = final_message + self._response = None + self._iterator = None + + async def __aenter__(self): + """ + Asynchronously enters the context for the streaming response. + + It awaits the response, checks for HTTP errors, and if an error occurs, + reads the content and raises an OrchestrationError. + + return: Self, with the streaming response stored. + rtype: AsyncSSEClient + """ + self._response = await self.response_cm.__aenter__() + try: + self._response.raise_for_status() + except httpx.HTTPStatusError as error: + content = await self._response.aread() + error_response = httpx.Response( + status_code=self._response.status_code, + headers=self._response.headers, + content=content, + request=self._response.request, + ) + await self.response_cm.__aexit__(None, None, None) + _handle_http_error(error, error_response) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + Asynchronously exits the context, ensuring that the context manager is properly closed. + """ + await self.response_cm.__aexit__(exc_type, exc_val, exc_tb) + + def _process_line(self, line: str) -> "OrchestrationResponseStreaming": + """ + Process a single line and return parsed event data if valid. + + :param line: The line to process + :type line: str + :return: Parsed event data or None if line is invalid or end of stream + :rtype: OrchestrationResponseStreaming or None + """ + line = line.strip() + if not line or not line.startswith(self.event_prefix): + return None + event_data = line[len(self.event_prefix):] + return _parse_event_data(event_data, self.final_message) + + async def _internal_iterator(self) -> AsyncIterator: + """ + Internal asynchronous generator that yields parsed events from the HTTP stream. + """ + buffer = "" + async for chunk in self._response.aiter_text(): + buffer += chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + result = self._process_line(line) + if result is None: + if line.strip() == self.final_message or line.strip().endswith(self.final_message): + return + continue + yield result + # Process any remaining data in the buffer + if buffer: + result = self._process_line(buffer) + if result is not None: + yield result + + def __aiter__(self): + """ + Returns the async iterator (self). The initialization of the stream is deferred until the first + call to __anext__. + """ + return self + + async def __anext__(self): + """ + Asynchronously retrieves the next event from the stream. On the first call, it enters the asynchronous + context to start the stream. When the stream is exhausted or the final message is received, it properly + exits the context. + + return: The next parsed event from the stream. + rtype: OrchestrationResponseStreaming + raises StopAsyncIteration: When the stream is exhausted. + """ + if self._iterator is None: + # Lazily initialize the stream. + await self.__aenter__() + self._iterator = self._internal_iterator().__aiter__() + try: + return await self._iterator.__anext__() + except StopAsyncIteration: + await self.__aexit__(None, None, None) + raise StopAsyncIteration + + +def _handle_http_error(error, response: httpx.Response): + """Handles HTTP errors by raising an OrchestrationError with details from the response. + + :param error: the original HTTP error. + :type error: httpx.HTTPStatusError + :param response: the httpx.Response object containing error details incl. headers. + :type response: httpx.Response + :raises OrchestrationError: with information extracted from the response. + """ + if not response.content: + raise error + try: + error_content = response.json() + error_content["http_headers"] = response.headers + except ValueError as exc: + raise error from exc + raise OrchestrationError( + request_id=error_content.get("request_id"), + http_headers=error_content.get("http_headers"), + message=error_content.get("message"), + code=error_content.get("code"), + location=error_content.get("location"), + module_results=error_content.get("module_results", {}), + ) from error diff --git a/packages/gen/gen_ai_hub/orchestration/utils.py b/packages/gen/gen_ai_hub/orchestration/utils.py new file mode 100644 index 0000000..757755e --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration/utils.py @@ -0,0 +1,11 @@ +def load_text_file(file_path): + """Loads and returns the content of a text file. + + :param file_path: The path to the text file to be loaded. + :type file_path: str + :return: The content of the file as a string. + :rtype: str + """ + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() + diff --git a/packages/gen/gen_ai_hub/orchestration_v2/__init__.py b/packages/gen/gen_ai_hub/orchestration_v2/__init__.py new file mode 100644 index 0000000..4759069 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/__init__.py @@ -0,0 +1,80 @@ +from .models import * +from .service import OrchestrationService +from .exceptions import OrchestrationError, OrchestrationErrorList + +__all__ = [ + # azure_content_filter + "AzureContentFilter", "AzureContentSafetyInput", "AzureContentSafetyOutput", "AzureThreshold", + + # config + "ModuleConfig", "OrchestrationConfig", "OrchestrationConfigReference", + "CompletionRequestConfigurationReferenceByIdConfigRef", + "CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef", + + # content_filter + "ContentFilterProvider", "ContentFilter", "LlamaGuard38bFilterConfig", + "AzureContentSafetyInputFilterConfig", "AzureContentSafetyOutputFilterConfig", "FilteringStreamOptions", + + # content_filtering + "InputFiltering", "OutputFiltering", "FilteringModuleConfig", + + # data_masking + "DataMaskingProviderName", "MaskingMethod", "ProfileEntity", "DPIMethodConstant", "DPIMethodFabricatedData", + "DPICustomEntity", "DPIStandardEntity", "MaskGroundingInput", "MaskingProviderConfig", "MaskingModuleConfig", + + # document_grounding + "GroundingType", "DataRepositoryType", "DocumentGroundingFilter", "DocumentGroundingPlaceholders", + "DocumentGroundingConfig", "GroundingModuleConfig", "KeyValueListPair", "DocumentMetadataKeyValueListPairs", + "GroundingSearchConfig", + + # embeddings + "EmbeddingsEncodingFormat", "EmbeddingsInputType", "EmbeddingsModelParams", "EmbeddingsModelDetails", + "EmbeddingsModelConfig", "EmbeddingsModuleConfigs", "EmbeddingsOrchestrationConfig", "EmbeddingsInput", + "EmbeddingsUsage", "EmbeddingResult", "EmbeddingsResponse", "EmbeddingsPostResponse", "EmbeddingsRequest", + + # llama_guard_3_filter + "LlamaGuard38bFilter", + + # llm_model_details + "LLMModelDetails", + + # message + "SystemMessage", "UserMessage", "AssistantMessage", "ToolChatMessage", "DeveloperChatMessage", + "ChatMessage", "ResponseChatMessage", "FunctionCall", "MessageToolCall", + + # multimodal_items + "ImageDetailLevel", "TextPart", "ImageUrl", "ImagePart", "ContentPart", "ImageItem", + + # response + "PromptTokensDetails", "CompletionTokensDetails", "TokenUsage", "GenericModuleResult", "TopLogprob", + "ChatCompletionTokenLogprob", "ChoiceLogprobs", "LLMChoice", "StreamFunctionObject", "StreamToolCall", + "StreamDelta", "StreamLLMChoice", "Citation", "LLMModuleResult", "StreamLLMModuleResult", "ModuleResults", + "StreamModuleResults", "SAPAPIError", "SAPAPIErrorStreaming", "CompletionPostResponse", + "StreamCompletionPostResponse", "ErrorResponse", "ErrorResponseStreaming", "OrchestrationResponseWithRetries", + + # response_format + "ResponseFormatText", "ResponseFormatJsonObject", "ResponseFormatJsonSchema", "JSONResponseSchema", + + # streaming + "GlobalStreamOptions", + + # template + "Template", "PromptTemplatingModuleConfig", + + # template_ref + "TemplateRef", "TemplateRefByID", "TemplateRefByScenarioNameVersion", + + # tools + "python_type_to_json_type", "ChatCompletionTool", "FunctionObject", "FunctionTool", "function_tool", + + # translation + "TranslationConfig", "SAPDocumentTranslation", "SAPDocumentTranslationApplyToSelector", + "InputTranslationConfig", "OutputTranslationConfig", "SAPDocumentTranslationInput", + "SAPDocumentTranslationOutput", "TranslationModuleConfig", + + # OrchestrationService + "OrchestrationService", + + # Exceptions + "OrchestrationError", "OrchestrationErrorList" + ] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py b/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py new file mode 100644 index 0000000..927944b --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/exceptions.py @@ -0,0 +1,63 @@ +""" +Exceptions for the orchestration service module. +""" + +from gen_ai_hub.orchestration_v2.models.response import ModuleResults + +import httpx +from typing import Optional + + +class OrchestrationError(Exception): + """ + This exception is raised when an error occurs during the execution of the + orchestration service, typically due to incorrect usage, invalid configurations, + or issues with run parameters defined by the user. + """ + + def __init__( + self, + request_id: str, + headers: httpx.Headers, + message: str, + code: int, + location: str, + intermediate_results: ModuleResults | dict, + retries: int = 0, + ): + """Initializes the OrchestrationError with detailed context. + + :param request_id: unique identifier for the request that encountered the error. + :type request_id: str + :param headers: HTTP headers associated with the request, useful in case of e.g. rate limiting.. + :type headers: httpx.Headers + :param message: Detailed error message describing the issue. + :type message: str + :param code: Error code associated with the specific type of failure. + :type code: int + :param location: Specific component or step in the orchestration process where the error occurred. + :type location: str + :param intermediate_results: State information and partial results from various modules + at the time of the error, useful for debugging. + :type intermediate_results: ModuleResults + :param retries: Number of retries attempted before the error was raised. + :type retries: int, optional + :param errors: Raw error payload(s) from the API. Can contain multiple errors. + :type errors: Optional[list[dict[str, Any]]] + """ + self.request_id = request_id + self.headers = headers + self.message = message + self.code = code + self.location = location + self.intermediate_results = intermediate_results + self.retries = retries + super().__init__(message) + +class OrchestrationErrorList(Exception): + def __init__(self, errors: list[OrchestrationError]): + self.errors = errors + super().__init__(errors[0].message) + + +__all__ = ["OrchestrationError", "OrchestrationErrorList"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/__init__.py b/packages/gen/gen_ai_hub/orchestration_v2/models/__init__.py new file mode 100644 index 0000000..a7628d3 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/__init__.py @@ -0,0 +1,107 @@ +from .azure_content_filter import AzureContentSafetyInput, AzureContentSafetyOutput, AzureContentFilter, AzureThreshold +from .config import (ModuleConfig, OrchestrationConfig, OrchestrationConfigReference, + CompletionRequestConfigurationReferenceByIdConfigRef, + CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) +from .content_filter import (ContentFilterProvider, ContentFilter, LlamaGuard38bFilterConfig, + AzureContentSafetyInputFilterConfig, AzureContentSafetyOutputFilterConfig, + FilteringStreamOptions) +from .content_filtering import InputFiltering, OutputFiltering, FilteringModuleConfig +from .data_masking import (DataMaskingProviderName, MaskingMethod, ProfileEntity, DPIMethodConstant, + DPIMethodFabricatedData, DPICustomEntity, DPIStandardEntity, MaskGroundingInput, + MaskingProviderConfig, MaskingModuleConfig) +from .document_grounding import (GroundingType, DataRepositoryType, DocumentGroundingFilter, + DocumentGroundingPlaceholders, DocumentGroundingConfig, GroundingModuleConfig, + KeyValueListPair, DocumentMetadataKeyValueListPairs, GroundingSearchConfig) +from .embeddings import (EmbeddingsEncodingFormat, EmbeddingsInputType, EmbeddingsModelParams, EmbeddingsModelDetails, + EmbeddingsModelConfig, EmbeddingsModuleConfigs, EmbeddingsOrchestrationConfig, EmbeddingsInput, + EmbeddingsUsage, EmbeddingResult, EmbeddingsResponse, EmbeddingsPostResponse, EmbeddingsRequest) +from .llama_guard_3_filter import LlamaGuard38bFilter +from .llm_model_details import LLMModelDetails +from .message import (SystemMessage, UserMessage, AssistantMessage, ToolChatMessage, DeveloperChatMessage, ChatMessage, + ResponseChatMessage, FunctionCall, MessageToolCall) +from .multimodal_items import ImageDetailLevel, TextPart, ImageUrl, ImagePart, ContentPart, ImageItem +from .response import (PromptTokensDetails, CompletionTokensDetails, TokenUsage, GenericModuleResult, TopLogprob, + ChatCompletionTokenLogprob, ChoiceLogprobs, LLMChoice, StreamFunctionObject, StreamToolCall, + StreamDelta, StreamLLMChoice, Citation, LLMModuleResult, StreamLLMModuleResult, ModuleResults, + StreamModuleResults, SAPAPIError, SAPAPIErrorStreaming, CompletionPostResponse, + StreamCompletionPostResponse, ErrorResponse, ErrorResponseStreaming, OrchestrationResponseWithRetries) +from .response_format import ResponseFormatText, ResponseFormatJsonObject, ResponseFormatJsonSchema, JSONResponseSchema +from .streaming import GlobalStreamOptions +from .template import Template, PromptTemplatingModuleConfig +from .template_ref import TemplateRef, TemplateRefByID, TemplateRefByScenarioNameVersion +from .tools import python_type_to_json_type, ChatCompletionTool, FunctionObject, FunctionTool, function_tool +from .translation import (TranslationConfig, SAPDocumentTranslation, SAPDocumentTranslationApplyToSelector, + InputTranslationConfig, OutputTranslationConfig, SAPDocumentTranslationInput, + SAPDocumentTranslationOutput, TranslationModuleConfig) + + +__all__ = [ + # azure_content_filter + "AzureContentFilter", "AzureContentSafetyInput", "AzureContentSafetyOutput", "AzureThreshold", + + # config + "ModuleConfig", "OrchestrationConfig", "OrchestrationConfigReference", + "CompletionRequestConfigurationReferenceByIdConfigRef", + "CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef", + + # content_filter + "ContentFilterProvider", "ContentFilter", "LlamaGuard38bFilterConfig", + "AzureContentSafetyInputFilterConfig", "AzureContentSafetyOutputFilterConfig", "FilteringStreamOptions", + + # content_filtering + "InputFiltering", "OutputFiltering", "FilteringModuleConfig", + + # data_masking + "DataMaskingProviderName", "MaskingMethod", "ProfileEntity", "DPIMethodConstant", "DPIMethodFabricatedData", + "DPICustomEntity", "DPIStandardEntity", "MaskGroundingInput", "MaskingProviderConfig", "MaskingModuleConfig", + + # document_grounding + "GroundingType", "DataRepositoryType", "DocumentGroundingFilter", "DocumentGroundingPlaceholders", + "DocumentGroundingConfig", "GroundingModuleConfig", "KeyValueListPair", "DocumentMetadataKeyValueListPairs", + "GroundingSearchConfig", + + # embeddings + "EmbeddingsEncodingFormat", "EmbeddingsInputType", "EmbeddingsModelParams", "EmbeddingsModelDetails", + "EmbeddingsModelConfig", "EmbeddingsModuleConfigs", "EmbeddingsOrchestrationConfig", "EmbeddingsInput", + "EmbeddingsUsage", "EmbeddingResult", "EmbeddingsResponse", "EmbeddingsPostResponse", "EmbeddingsRequest", + + # llama_guard_3_filter + "LlamaGuard38bFilter", + + # llm_model_details + "LLMModelDetails", + + # message + "SystemMessage", "UserMessage", "AssistantMessage", "ToolChatMessage", "DeveloperChatMessage", + "ChatMessage", "ResponseChatMessage", "FunctionCall", "MessageToolCall", + + # multimodal_items + "ImageDetailLevel", "TextPart", "ImageUrl", "ImagePart", "ContentPart", "ImageItem", + + # response + "PromptTokensDetails", "CompletionTokensDetails", "TokenUsage", "GenericModuleResult", "TopLogprob", + "ChatCompletionTokenLogprob", "ChoiceLogprobs", "LLMChoice", "StreamFunctionObject", "StreamToolCall", + "StreamDelta", "StreamLLMChoice", "Citation", "LLMModuleResult", "StreamLLMModuleResult", "ModuleResults", + "StreamModuleResults", "SAPAPIError", "SAPAPIErrorStreaming", "CompletionPostResponse", + "StreamCompletionPostResponse", "ErrorResponse", "ErrorResponseStreaming", "OrchestrationResponseWithRetries", + + # response_format + "ResponseFormatText", "ResponseFormatJsonObject", "ResponseFormatJsonSchema", "JSONResponseSchema", + + # streaming + "GlobalStreamOptions", + + # template + "Template", "PromptTemplatingModuleConfig", + + # template_ref + "TemplateRef", "TemplateRefByID", "TemplateRefByScenarioNameVersion", + + # tools + "python_type_to_json_type", "ChatCompletionTool", "FunctionObject", "FunctionTool", "function_tool", + + # translation + "TranslationConfig", "SAPDocumentTranslation", "SAPDocumentTranslationApplyToSelector", + "InputTranslationConfig", "OutputTranslationConfig", "SAPDocumentTranslationInput", + "SAPDocumentTranslationOutput", "TranslationModuleConfig" +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/azure_content_filter.py b/packages/gen/gen_ai_hub/orchestration_v2/models/azure_content_filter.py new file mode 100644 index 0000000..cedbc2e --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/azure_content_filter.py @@ -0,0 +1,83 @@ +""" +Azure Content Filter Model +""" + +from enum import Enum +from typing import Union, Literal, Optional + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class AzureThreshold(int, Enum): + """ + Enumerates the threshold levels for the Azure Content Safety service. + + This enum defines the various threshold levels that can be used to filter + content based on its safety score. Each threshold value represents a specific + level of content moderation. + + Values: + ALLOW_SAFE: Allows only Safe content. + ALLOW_SAFE_LOW: Allows Safe and Low content. + ALLOW_SAFE_LOW_MEDIUM: Allows Safe, Low, and Medium content. + ALLOW_ALL: Allows all content (Safe, Low, Medium, and High). + """ + + ALLOW_SAFE = 0 + ALLOW_SAFE_LOW = 2 + ALLOW_SAFE_LOW_MEDIUM = 4 + ALLOW_ALL = 6 + + +class AzureContentFilter(BaseModel): + """ + Specific filter configuration for Azure Content Safety. + + This class configures content filtering based on Azure's categories and + severity levels. It allows setting thresholds for hate speech, sexual content, + violence, and self-harm content. + + Args: + hate: Threshold for hate speech content. + sexual: Threshold for sexual content. + violence: Threshold for violent content. + self_harm: Threshold for self-harm content. + prompt_shield: A flag to use prompt shield + """ + + hate: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + sexual: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + violence: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + self_harm: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + +class AzureContentSafetyInput(AzureContentFilter): + """ + Filter configuration for Azure Content Safety Input + + Args: + hate: Threshold for hate speech content. + sexual: Threshold for sexual content. + violence: Threshold for violent content. + self_harm: Threshold for self-harm content. + prompt_shield: A flag to use prompt shield + """ + prompt_shield: Optional[bool] = False + + +class AzureContentSafetyOutput(AzureContentFilter): + """ + Filter configuration for Azure Content Safety Output + + Args: + hate: Threshold for hate speech content. + sexual: Threshold for sexual content. + violence: Threshold for violent content. + self_harm: Threshold for self-harm content. + protected_material_code: Detect protected code content from known GitHub repositories. + The scan includes software libraries, source code, algorithms, + and other proprietary programming content. + """ + + protected_material_code: Optional[bool] = False + +__all__ = ["AzureContentFilter", "AzureContentSafetyInput", "AzureContentSafetyOutput", "AzureThreshold"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/base.py b/packages/gen/gen_ai_hub/orchestration_v2/models/base.py new file mode 100644 index 0000000..4610461 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/base.py @@ -0,0 +1,33 @@ +""" +A base model class that extends Pydantic's BaseModel and ABC. +""" + +from abc import ABC + +from pydantic import BaseModel, ConfigDict + + +class ABCBaseModel(BaseModel, ABC): + """ + Abstract base model that extends Pydantic's BaseModel and ABC. + + - `extra="forbid"` prevents unexpected fields from being accepted or sent, + since the external API is strict about allowed parameters. + - `exclude_none=True` ensures optional fields that were not provided are + omitted from the payload instead of being serialized as None. + - `by_alias=True` guarantees aliases are used during serialization, which is + required because some API field names conflict with Pydantic's built-ins. + + This enforces consistent and safe serialization behavior across all + derived models. + """ + model_config = ConfigDict( + extra="forbid", + frozen=False, + ) + + def model_dump(self, **kwargs): + """Dumps the model to a dictionary with default settings.""" + kwargs.setdefault("by_alias", True) + kwargs.setdefault("exclude_none", True) + return super().model_dump(**kwargs) diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/config.py b/packages/gen/gen_ai_hub/orchestration_v2/models/config.py new file mode 100644 index 0000000..84d6351 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/config.py @@ -0,0 +1,90 @@ +""" +Orchestration Service configuration models. +""" + +from typing import Annotated, List, Optional + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.content_filtering import FilteringModuleConfig +from gen_ai_hub.orchestration_v2.models.data_masking import MaskingModuleConfig +from gen_ai_hub.orchestration_v2.models.document_grounding import GroundingModuleConfig +from gen_ai_hub.orchestration_v2.models.streaming import GlobalStreamOptions +from gen_ai_hub.orchestration_v2.models.template import PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.translation import TranslationModuleConfig + + +class ModuleConfig(BaseModel): + """ + Configuration for the Orchestration Service's content generation process. + + Defines modules for a harmonized API that combines LLM-based content generation + with additional processing functionalities. + + The orchestration service allows for advanced content generation by processing inputs through a series of steps: + template rendering, text generation via LLMs, and optional input/output transformations such as data masking + or filtering. + + Args: + prompt_templating: Template object for rendering input prompts and language model for text generation. + + filtering: Module for filtering and validating input/output content. + + masking: Module for anonymizing or pseudonymizing sensitive information. + + grounding: Module for document grounding. + + translation: Module for translating input and output content. + """ + + prompt_templating: PromptTemplatingModuleConfig + filtering: Optional[FilteringModuleConfig] = None + masking: Optional[MaskingModuleConfig] = None + grounding: Optional[GroundingModuleConfig] = None + translation: Optional[TranslationModuleConfig] = None + + +class OrchestrationConfig(BaseModel): + """ + Configuration for the Orchestration Service's content generation process. + + Args: + modules: Either a single ModuleConfig or a list of ModuleConfigs. When a list is provided, + the orchestration service will try each configuration in order until one succeeds. + + stream: Optional streaming configuration. + """ + modules: ModuleConfig | Annotated[List[ModuleConfig], Field(min_length=1)] + stream: Optional[GlobalStreamOptions] = None + + +class CompletionRequestConfigurationReferenceByIdConfigRef(BaseModel): + """Represents a reference to an orchestration config identified by a unique ID. + + Args: + id (str): The unique identifier for the configuration. + """ + id: str + +class CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef(BaseModel): + """ + Represents a reference to aan orchestration config identified by name, scenario, and version. + + Args: + scenario(str): Scenario name + + name(str): Name of config + + version(str): Version of config + """ + scenario: str + name: str + version: str + +OrchestrationConfigReference = (CompletionRequestConfigurationReferenceByIdConfigRef | + CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) + +__all__ = ["ModuleConfig", "OrchestrationConfig", "OrchestrationConfigReference", + "CompletionRequestConfigurationReferenceByIdConfigRef", + "CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/content_filter.py b/packages/gen/gen_ai_hub/orchestration_v2/models/content_filter.py new file mode 100644 index 0000000..5c235d5 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/content_filter.py @@ -0,0 +1,68 @@ +""" +Content filter models for various providers. +""" + +from enum import Enum +from typing import Optional, Union + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.azure_content_filter import (AzureContentSafetyInput, AzureContentSafetyOutput, + AzureContentFilter) +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.llama_guard_3_filter import LlamaGuard38bFilter + + +class ContentFilterProvider(str, Enum): + """ + Enumerates supported content filter providers. + + This enum defines the available content filtering services that can be used + for content moderation tasks. Each enum value represents a specific provider. + + Values: + AZURE: Represents the Azure Content Safety service. + + LLAMA_GUARD_3_8B: Represents the Llama Guard 3 based on Llama-3.1-8B pretrained model. + """ + + AZURE = "azure_content_safety" + LLAMA_GUARD_3_8B = "llama_guard_3_8b" + +class ContentFilter(BaseModel): + """ + Base class for content filtering configurations. + + This class provides a generic structure for defining content filters + from various providers. It allows for specifying the provider and + associated configuration parameters. + + Args: + type: The name of the content filter provider. + + config: A dictionary containing the configuration parameters for the content filter. + """ + type_: ContentFilterProvider = Field(..., alias="type") + config: Optional[Union[AzureContentFilter, LlamaGuard38bFilter]] = None + +class LlamaGuard38bFilterConfig(ContentFilter): + type_: ContentFilterProvider = Field(default=ContentFilterProvider.LLAMA_GUARD_3_8B, alias="type") + config: LlamaGuard38bFilter + +class AzureContentSafetyInputFilterConfig(ContentFilter): + type_: ContentFilterProvider = Field(default=ContentFilterProvider.AZURE, alias="type") + config: Optional[AzureContentSafetyInput] = None + +class AzureContentSafetyOutputFilterConfig(ContentFilter): + type_: ContentFilterProvider = Field(default=ContentFilterProvider.AZURE, alias="type") + config: Optional[AzureContentSafetyOutput] = None + +class FilteringStreamOptions(BaseModel): + """ + overlap: Number of characters that should be additionally sent to content filtering services + from previous chunks as additional context. + """ + overlap: Optional[int] = Field(default=0, ge=0, le=10000) + +__all__ = ["ContentFilterProvider", "ContentFilter", "LlamaGuard38bFilterConfig", "AzureContentSafetyInputFilterConfig", + "AzureContentSafetyOutputFilterConfig", "FilteringStreamOptions"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/content_filtering.py b/packages/gen/gen_ai_hub/orchestration_v2/models/content_filtering.py new file mode 100644 index 0000000..e7b9906 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/content_filtering.py @@ -0,0 +1,61 @@ +""" +Module for managing and applying content filters in the orchestration system. +""" + +from typing import List, Optional, Union + +from pydantic import model_validator, Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.content_filter import (AzureContentSafetyOutputFilterConfig, +AzureContentSafetyInputFilterConfig, LlamaGuard38bFilterConfig, FilteringStreamOptions, ContentFilter) + + +class InputFiltering(BaseModel): + """Module for managing and applying input content filters. + + Args: + filters: List of ContentFilter objects to be applied to input content. + """ + filters: List[ + Union[AzureContentSafetyInputFilterConfig, LlamaGuard38bFilterConfig, ContentFilter] + ] = Field(min_length=1) + + +class OutputFiltering(BaseModel): + """Module for managing and applying output content filters. + + Args: + filters: List of ContentFilter objects to be applied to output content. + + stream_options: Module-specific streaming options. + """ + + filters: List[ + Union[AzureContentSafetyOutputFilterConfig, LlamaGuard38bFilterConfig, ContentFilter] + ] = Field(min_length=1) + stream_options: Optional[FilteringStreamOptions] = None + + +class FilteringModuleConfig(BaseModel): + """Module for managing and applying content filters. + + Args: + input: Module for filtering and validating input content before processing. + + output: Module for filtering and validating output content after generation. + """ + + input: Optional[InputFiltering] = None + output: Optional[OutputFiltering] = None + + @model_validator(mode="after") + def enforce_min_properties(cls, values): # pylint: disable=no-self-argument + """ + Ensure at least one of input or output filtering is provided. + """ + assert values.input is not None or values.output is not None, \ + "FilteringModuleConfig must have at least one property: input or output." + return values + +__all__ = ["InputFiltering", "OutputFiltering", "FilteringModuleConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/data_masking.py b/packages/gen/gen_ai_hub/orchestration_v2/models/data_masking.py new file mode 100644 index 0000000..ffcc5a3 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/data_masking.py @@ -0,0 +1,221 @@ +# pylint: disable=duplicate-code +""" +Data Masking Module Configuration Models +""" + +from enum import Enum +from typing import Optional, Union, List + +from pydantic import Field, model_validator + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class DataMaskingProviderName(str, Enum): + """ + Enumerates the available data masking providers. + + This enum defines the supported providers for masking sensitive data in the LLM module. + + Values: SAP_DATA_PRIVACY_INTEGRATION: Refers to the SAP Data Privacy Integration service, which offers + anonymization and pseudonymization capabilities for sensitive data. + """ + SAP_DATA_PRIVACY_INTEGRATION = "sap_data_privacy_integration" + + +class MaskingMethod(str, Enum): + """ + Enumerates the supported masking methods. + + This enum defines the two main methods for masking sensitive information: anonymization and pseudonymization. + Anonymization irreversibly removes sensitive data, while pseudonymization allows the original data to be recovered. + + Values: + ANONYMIZATION: Irreversibly replaces sensitive data with placeholders (e.g., MASKED_ENTITY). + + PSEUDONYMIZATION: Replaces sensitive data with reversible placeholders (e.g., MASKED_ENTITY_ID). + """ + ANONYMIZATION = "anonymization" + PSEUDONYMIZATION = "pseudonymization" + + +class ProfileEntity(str, Enum): + """ + Enumerates the entity categories that can be masked by the SAP Data Privacy Integration service. + + This enum lists different types of personal or sensitive information (PII) that can be detected and masked + by the data masking module, such as personal details, organizational data, contact information, and identifiers. + + Values: + PERSON: Represents personal names. + + ORG: Represents organizational names. + + UNIVERSITY: Represents educational institutions. + + LOCATION: Represents geographical locations. + + EMAIL: Represents email addresses. + + PHONE: Represents phone numbers. + + ADDRESS: Represents physical addresses. + + SAP_IDS_INTERNAL: Represents internal SAP identifiers. + + SAP_IDS_PUBLIC: Represents public SAP identifiers. + + URL: Represents URLs. + + USERNAME_PASSWORD: Represents usernames and passwords. + + NATIONAL_ID: Represents national identification numbers. + + IBAN: Represents International Bank Account Numbers. + + SSN: Represents Social Security Numbers. + + CREDIT_CARD_NUMBER: Represents credit card numbers. + + PASSPORT: Represents passport numbers. + + DRIVING_LICENSE: Represents driving license numbers. + + NATIONALITY: Represents nationality information. + + RELIGIOUS_GROUP: Represents religious group affiliation. + + POLITICAL_GROUP: Represents political group affiliation. + + PRONOUNS_GENDER: Represents pronouns and gender identity. + + GENDER: Represents gender information. + + SEXUAL_ORIENTATION: Represents sexual orientation. + + TRADE_UNION: Represents trade union membership. + + SENSITIVE_DATA: Represents any other sensitive information. + """ + + PERSON = "profile-person" + ORG = "profile-org" + UNIVERSITY = "profile-university" + LOCATION = "profile-location" + EMAIL = "profile-email" + PHONE = "profile-phone" + ADDRESS = "profile-address" + SAP_IDS_INTERNAL = "profile-sapids-internal" + SAP_IDS_PUBLIC = "profile-sapids-public" + URL = "profile-url" + USERNAME_PASSWORD = "profile-username-password" + NATIONAL_ID = "profile-nationalid" + IBAN = "profile-iban" + SSN = "profile-ssn" + CREDIT_CARD_NUMBER = "profile-credit-card-number" + PASSPORT = "profile-passport" + DRIVING_LICENSE = "profile-driverlicense" + NATIONALITY = "profile-nationality" + RELIGIOUS_GROUP = "profile-religious-group" + POLITICAL_GROUP = "profile-political-group" + PRONOUNS_GENDER = "profile-pronouns-gender" + GENDER = "profile-gender" + SEXUAL_ORIENTATION = "profile-sexual-orientation" + TRADE_UNION = "profile-trade-union" + SENSITIVE_DATA = "profile-sensitive-data" + ETHNICITY = "profile-ethnicity" + + +class DPIMethodConstant(BaseModel): + """ + Replaces the entity with the specified value followed by an incrementing number + """ + method: str = "constant" + value: str + + +class DPIMethodFabricatedData(BaseModel): + """ + Replaces the entity with a randomly generated value appropriate to its type. + """ + method: str = "fabricated_data" + + +class DPICustomEntity(BaseModel): + """ + regex: Regular expression to match the entity + replacement_strategy: Replacement strategy to be used for the entity + """ + regex: str + replacement_strategy: DPIMethodConstant + + +class DPIStandardEntity(BaseModel): + """ + type: Standard entity type to be masked + replacement_strategy: Replacement strategy to be used for the entity + """ + type_: ProfileEntity = Field(..., alias="type") + replacement_strategy: Optional[Union[DPIMethodConstant, DPIMethodFabricatedData]] = None + + +class MaskGroundingInput(BaseModel): + """ + Controls whether the input to the grounding module will be masked with the configuration + supplied in the masking module + """ + enabled: bool = False + + +class MaskingProviderConfig(BaseModel): + """ + SAP Data Privacy Integration provider for data masking. + + This class implements the SAP Data Privacy Integration service, which can anonymize or pseudonymize + specified entity categories in the input data. It supports masking sensitive information like personal names, + contact details, and identifiers. + + Args: + method: The method of masking to apply (anonymization or pseudonymization). + + entities: A list of entity categories to be masked, such as names, locations, or emails. + + allowlist: A list of strings that should not be masked. + + mask_grounding_input: A flag indicating whether to mask input to the grounding module. + """ + type_: DataMaskingProviderName = Field(default=DataMaskingProviderName.SAP_DATA_PRIVACY_INTEGRATION, alias="type") + method: MaskingMethod + entities: List[Union[DPIStandardEntity, DPICustomEntity]] + allowlist: Optional[List[str]] = None + mask_grounding_input: Optional[MaskGroundingInput] = None + +class MaskingModuleConfig(BaseModel): + """ + Configuration for the data masking module. + + Args: + providers: list of masking service provider configurations + masking_providers: list of masking provider configurations + IMPORTANT: use exactly one of the parameters to set the list of masking provider configurations. + DEPRECATED: parameter 'masking_providers' will be removed Sept 15, 2026. Use 'providers' instead. + """ + providers: Optional[List[MaskingProviderConfig]] = Field(min_length=1, default=None) + masking_providers: Optional[List[MaskingProviderConfig]] = Field(min_length=1, default=None) + + @model_validator(mode="after") + def enforce_exactly_one_provider_list(self): + + has_providers = self.providers is not None + has_masking_providers = self.masking_providers is not None + + if has_providers == has_masking_providers: + raise ValueError( + "MaskingModuleConfigProviders must set exactly one of: 'providers' or 'masking_providers' " + "DEPRECATED: parameter 'masking_providers' will be removed Sept 15, 2026. Use 'providers' instead." + ) + + return self + +__all__ = ["DataMaskingProviderName", "MaskingMethod", "ProfileEntity", "DPIMethodConstant", "DPIMethodFabricatedData", + "DPICustomEntity", "DPIStandardEntity", "MaskGroundingInput", "MaskingProviderConfig", "MaskingModuleConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/document_grounding.py b/packages/gen/gen_ai_hub/orchestration_v2/models/document_grounding.py new file mode 100644 index 0000000..e229440 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/document_grounding.py @@ -0,0 +1,137 @@ +""" +Module for defining document grounding configurations. +""" + +from enum import Enum +from typing import List, Literal, Optional, Union + +from pydantic import Field, model_validator, ValidationError + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class GroundingType(str, Enum): + """ + Enumerates supported grounding types. + """ + DOCUMENT_GROUNDING_SERVICE = "document_grounding_service" + + +class DataRepositoryType(str, Enum): + """ + Enumerates data repository types. + """ + VECTOR = "vector" # DataRepository with vector embeddings + URL = "help.sap.com" # website supporting elastic search + + +class KeyValueListPair(BaseModel): + key: str + value: List[str] + + +class DocumentMetadataKeyValueListPairs(KeyValueListPair): + """Restrict documents considered during search to those annotated with the given metadata. + + Args: + key: The key for the metadata. + + value: The list of values for the metadata. + + select_mode: Select mode for search filters. + """ + select_mode: Optional[List[Literal['ignoreIfKeyAbsent']]] = None + + +class GroundingSearchConfig(BaseModel): + """Search configuration for the data repository. + + Args: + max_chunk_count(int, minimum: 0, exclusiveMinimum: true): Maximum number of chunks to be returned. + Cannot be used with 'maxDocumentCount'. + + max_document_count(int, minimum: 0, exclusiveMinimum: true): [Only supports 'vector' dataRepositoryType] + - Maximum number of documents to be returned. Cannot be used with 'maxChunkCount'. + If maxDocumentCount is given, then only one chunk per document is returned. + """ + max_chunk_count: Optional[int] = Field(default=None, gt=0) + max_document_count: Optional[int] = Field(default=None, gt=0) + + @model_validator(mode='after') + def validate_max_chunk_count_and_max_document_count(self): + if self.max_chunk_count and self.max_document_count: + raise ValueError("Cannot specify both maxChunkCount and maxDocumentCount.") + return self + + +class DocumentGroundingFilter(BaseModel): + """Module for configuring document grounding filters. + + Args: + id: The unique identifier for the grounding filter. + + search_config: GroundingSearchConfig object. + + data_repository_type: Only include DataRepositories with the given type: + vector, help.sap.com. + + data_repositories: list of data repositories to search. + Specify ['*'] to search across all DataRepositories or + give a specific list of DataRepository ids. + + data_repository_metadata: The metadata for the data repository. + Restrict DataRepositories considered during search to those annotated with the given + metadata. Useful when combined with dataRepositories=['*'] + + document_metadata: DocumentMetadata object. + + chunk_metadata: Restrict chunks considered during search to those with the given metadata. + """ + + id: Optional[str] = None + data_repository_type: Union[DataRepositoryType, Literal["vector", "help.sap.com"]] + search_config: Optional[GroundingSearchConfig] = None + data_repositories: Optional[List[str]] = None + data_repository_metadata: Optional[List[KeyValueListPair]] = None + document_metadata: Optional[List[DocumentMetadataKeyValueListPairs]] = None + chunk_metadata: Optional[List[KeyValueListPair]] = None + + +class DocumentGroundingPlaceholders(BaseModel): + """ + input: The list of input parameters used for grounding input questions (minItems: 1). + output: Parameter name used for grounding output. + """ + input: List[str] = Field(min_length=1) + output: str + + +class DocumentGroundingConfig(BaseModel): + """defines the detailed configuration for the Grounding module. + + Args: + filters: List of DocumentGroundingFilter objects. + + placeholders: Placeholders to be used for grounding input questions and output. + + metadata_params: Parameter name used for specifying metadata parameters. + """ + filters: Optional[List[DocumentGroundingFilter]] = None + placeholders: DocumentGroundingPlaceholders + metadata_params: Optional[list[str]] = None + + +class GroundingModuleConfig(BaseModel): + """Module for managing and applying grounding aka RAG configurations. + + Args: + type: The type of the grounding module. + + config: Configuration dictionary for the grounding module. + """ + + type: GroundingType = GroundingType.DOCUMENT_GROUNDING_SERVICE + config: DocumentGroundingConfig + +__all__ = ["GroundingType", "DataRepositoryType", "DocumentGroundingFilter", "DocumentGroundingPlaceholders", + "DocumentGroundingConfig", "GroundingModuleConfig", "KeyValueListPair", "DocumentMetadataKeyValueListPairs"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/embeddings.py b/packages/gen/gen_ai_hub/orchestration_v2/models/embeddings.py new file mode 100644 index 0000000..28fd469 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/embeddings.py @@ -0,0 +1,189 @@ +""" +Embeddings Module Configuration Models +""" + +from enum import Enum +from typing import Optional, Dict, List, Union + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.data_masking import MaskingModuleConfig + + +class EmbeddingsEncodingFormat(str, Enum): + """ + Encoding format for the embeddings output. + + Values: + FLOAT: Returns embeddings as an array of floats. + BASE64: Returns embeddings as a base64 encoded string. + BINARY: Returns embeddings in binary format. + """ + FLOAT = "float" + BASE64 = "base64" + BINARY = "binary" + + +class EmbeddingsInputType(str, Enum): + """ + Type hint for the embedding model about the purpose of the text. + + Some models use asymmetric embeddings for better search performance. + + Values: + TEXT: General purpose text (default). + DOCUMENT: Content to be searched/retrieved. + QUERY: Short search queries. + """ + TEXT = "text" + DOCUMENT = "document" + QUERY = "query" + + +class EmbeddingsModelParams(BaseModel): + """ + Additional parameters for generating embeddings. + + Args: + dimensions: The number of dimensions for the output embeddings. + encoding_format: The format for the embeddings output (float, base64, or binary). + normalize: Whether to normalize the embeddings. + """ + dimensions: Optional[int] = None + encoding_format: Optional[EmbeddingsEncodingFormat] = None + normalize: Optional[bool] = None + + +class EmbeddingsModelDetails(BaseModel): + """ + The model and parameters to be used for generating embeddings. + + Args: + name: Name of the embedding model. + version: Version of the model to be used. Defaults to "latest". + params: Additional parameters for the model (dimensions, encoding_format, normalize). + timeout: Timeout for the embeddings request in seconds. Ignored for Vertex AI models. + max_retries: Maximum number of retries. Ignored for Vertex AI models. + """ + name: str + version: Optional[str] = "latest" + params: Optional[EmbeddingsModelParams] = None + timeout: Optional[int] = Field(default=600, ge=1, le=600) + max_retries: Optional[int] = Field(default=2, ge=0, le=5) + + +class EmbeddingsModelConfig(BaseModel): + """ + Configuration for the embeddings model. + + Args: + model: The embedding model details. + """ + model: EmbeddingsModelDetails + + +class EmbeddingsModuleConfigs(BaseModel): + """ + Module configurations for the embeddings endpoint. + + Args: + embeddings: Required configuration for the embeddings model. + masking: Optional configuration for data masking before embedding. + """ + embeddings: EmbeddingsModelConfig + masking: Optional[MaskingModuleConfig] = None + + +class EmbeddingsOrchestrationConfig(BaseModel): + """ + Configuration for the Embeddings Orchestration endpoint. + + Args: + modules: The module configurations including embeddings model and optional masking. + """ + modules: EmbeddingsModuleConfigs + + +class EmbeddingsInput(BaseModel): + """ + Input for the embeddings endpoint. + + Args: + text: The text to embed. Can be a single string or a list of strings. + type: Optional type hint for the embedding model (text, document, or query). + """ + text: Union[str, List[str]] + type_: Optional[EmbeddingsInputType] = Field(default=None, alias="type") + + +class EmbeddingsUsage(BaseModel): + """ + Token usage information for the embeddings request. + + Args: + prompt_tokens: The number of tokens used by the prompt. + total_tokens: The total number of tokens used by the request. + """ + prompt_tokens: int + total_tokens: int + + +class EmbeddingResult(BaseModel): + """ + A single embedding result. + + Args: + object: The object type, always "embedding". + embedding: The embedding vector (array of floats) or base64 string. + index: The index of this embedding in the list. + """ + object: str = "embedding" + embedding: Union[List[float], str] + index: int + + +class EmbeddingsResponse(BaseModel): + """ + The response from the embedding model, following OpenAI specification. + + Args: + object: The object type, always "list". + data: The list of embeddings generated by the model. + model: The name of the model used to generate the embeddings. + usage: Token usage information. + """ + object: str = "list" + data: List[EmbeddingResult] + model: str + usage: EmbeddingsUsage + + +class EmbeddingsPostResponse(BaseModel): + """ + Response for an embeddings POST request. + + Args: + request_id: Unique identifier for the request. + intermediate_results: Optional results from intermediate modules (e.g., masking). + final_result: The embeddings response from the model. + """ + request_id: str + intermediate_results: Optional[Dict] = None + final_result: EmbeddingsResponse + + +class EmbeddingsRequest(BaseModel): + """ + Request body for the embeddings endpoint. + + Args: + config: The embeddings orchestration configuration. + input: The input text to embed. + """ + config: EmbeddingsOrchestrationConfig + input: EmbeddingsInput + +__all__ = ["EmbeddingsEncodingFormat", "EmbeddingsInputType", "EmbeddingsModelParams", "EmbeddingsModelDetails", + "EmbeddingsModelConfig", "EmbeddingsModuleConfigs", "EmbeddingsOrchestrationConfig", "EmbeddingsInput", + "EmbeddingsUsage", "EmbeddingResult", "EmbeddingsResponse", "EmbeddingsPostResponse", "EmbeddingsRequest"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/llama_guard_3_filter.py b/packages/gen/gen_ai_hub/orchestration_v2/models/llama_guard_3_filter.py new file mode 100644 index 0000000..9707bc6 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/llama_guard_3_filter.py @@ -0,0 +1,58 @@ +""" +Llama Guard 3 Content Filter Model +""" + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class LlamaGuard38bFilter(BaseModel): + """ + Specific implementation of ContentFilter for Llama Guard 3. Llama Guard 3 is a + Llama-3.1-8B pretrained model, fine-tuned for content safety classification. + + Args: + violent_crimes: Responses that enable, encourage, or endorse the commission of violent crimes. + + non_violent_crimes: Responses that enable, encourage, or endorse the commission of non-violent crimes. + + sex_crimes: Responses that enable, encourage, or endorse the commission of sex-related crimes. + + child_exploitation: Responses that contain, describe, enable or endorse the sexual abuse of children. + + defamation: Responses that are both verifiably false and likely to injure a living person's reputation. + + specialized_advice: Responses that contain specialized financial, medical or legal advice. + + privacy: Responses that contain sensitive or nonpublic personal information. + + intellectual_property: Responses that may violate the intellectual property rights of any third party. + + indiscriminate_weapons: Responses that enable, encourage, or endorse the creation of indiscriminate weapons. + + hate: Responses that demean or dehumanize people on the basis of their sensitive, personal characteristics. + + self_harm: Responses that enable, encourage, or endorse acts of intentional self-harm. + + sexual_content: Responses that contain erotica. + + elections: Responses that contain factually incorrect information about electoral systems and processes. + + code_interpreter_abuse: Responses that seek to abuse code interpreters. + """ + + violent_crimes: bool = False + non_violent_crimes: bool = False + sex_crimes: bool = False + child_exploitation: bool = False + defamation: bool = False + specialized_advice: bool = False + privacy: bool = False + intellectual_property: bool = False + indiscriminate_weapons: bool = False + hate: bool = False + self_harm: bool = False + sexual_content: bool = False + elections: bool = False + code_interpreter_abuse: bool = False + +__all__ = ["LlamaGuard38bFilter"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/llm_model_details.py b/packages/gen/gen_ai_hub/orchestration_v2/models/llm_model_details.py new file mode 100644 index 0000000..0538635 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/llm_model_details.py @@ -0,0 +1,34 @@ +""" +Module defining the LLM (Large Language Model) configuration model. +""" + +from typing import Optional, Dict + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class LLMModelDetails(BaseModel): + """ + The model and parameters to be used for the prompt templating. + This is the model that will be used to generate the response. + + Args: + name: Name of the model as in LLM Access configuration. + + version: Version of the model to be used. Defaults to "latest". + + params: Additional parameters for the model. Default values are used for mandatory parameters. + + timeout: Timeout for the LLM request in seconds. This parameter is currently ignored for Vertex AI models. + + max_retries: Maximum number of retries for the LLM request. This parameter is currently ignored for Vertex AI models. + """ + name: str + version: Optional[str] = "latest" + params: Optional[Dict] = None + timeout: Optional[int] = Field(default=600, ge=1, le=600) + max_retries: Optional[int] = Field(default=2, ge=0, le=5) + +__all__ = ["LLMModelDetails"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/message.py b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py new file mode 100644 index 0000000..aa5039c --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/message.py @@ -0,0 +1,195 @@ +""" +Defines message-related models for LLM-based conversations, +including system, user, assistant, tool, and developer messages. +""" + +import json +import typing +from enum import Enum +from typing import Union, Optional, List + +from pydantic import field_validator, ValidationError + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.multimodal_items import ContentPart, ImageItem, TextPart, ImageUrl, ImagePart + + +class FunctionCall(BaseModel): + """ + Represents a function call with its name and arguments. + + Attributes: + name: str + The name of the function to call. + + arguments: str + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + name: str + arguments: str + + def parse_arguments(self) -> dict: + """Parses the arguments string as JSON. + + :return: A dictionary representing the parsed arguments. + :rtype: dict + """ + + if self.arguments is None: + return {} + + return json.loads(self.arguments) + + +class MessageToolCall(BaseModel): + """ + The tool calls generated by the model, such as function calls. + + Attributes: + id: The ID of the tool call. + + type: The type of the tool. Currently, only `function` is supported. + + function: The function that the model called. + """ + id: str + type: typing.Literal["function"] = "function" + function: FunctionCall + + +class Role(str, Enum): + """ + Enumerates supported roles in LLM-based conversations. + + This enum defines the standard roles used in interactions with Large Language Models (LLMs). + These roles are generally used to structure the input and distinguish between different parts of the conversation. + + Values: + USER: Represents the human user's input in the conversation. + + SYSTEM: Represents system-level instructions or context setting for the LLM. + + ASSISTANT: Represents the LLM's responses in the conversation. + + TOOL: Represents a tool or function that the LLM can call. + + DEVELOPER: Represents the developer's input or instructions in the conversation. + """ + + USER = "user" + SYSTEM = "system" + ASSISTANT = "assistant" + TOOL = "tool" + DEVELOPER = "developer" + + +class SystemMessage(BaseModel): + """ + Represents a system message in a prompt or conversation template. + + System messages typically provide context or instructions to the AI model. + + Args: + role: The role of the entity sending the message. + + content: The text content of the system message. + """ + role: Role = Role.SYSTEM + content: Union[str, List[TextPart]] + + +class UserMessage(BaseModel): + """ + Represents a user message in a prompt or conversation template. + + User messages typically contain queries or inputs from the user. + + Args: + role: The role of the entity sending the message. + + content: The message content, which may be plain text or a sequence of text and images. + """ + role: Role = Role.USER + content: Union[str, ContentPart, List[Union[str, ContentPart, ImageItem, ImagePart]]] + + @field_validator("content", mode="before") + def content_validation(cls, content): # pylint: disable=no-self-argument + """ + Validates and maps the content field to the appropriate types. + """ + + mapped_content = [] + + if isinstance(content, (str, ContentPart, dict)): + mapped_content = content + elif isinstance(content, list): + for item in content: + if isinstance(item, (ContentPart, dict)): + mapped_content.append(item) + elif isinstance(item, str): + mapped_content.append(TextPart(text=item)) + elif isinstance(item, ImageItem): + mapped_content.append(ImagePart(image_url=ImageUrl(url=item.url, detail=item.detail))) + else: + raise ValidationError("User message content list must contain only str or ImageItem") + return mapped_content + + +class AssistantMessage(BaseModel): + """ + Represents an assistant message in a prompt or conversation template. + + Assistant messages typically contain responses or outputs from the AI model. + + Args: + role: The role of the entity sending the message. + + content: The text content of the assistant message. + + refusal: A string indicating refusal reason. + + tool_calls: A list of tool call objects. + """ + role: Role = Role.ASSISTANT + content: Optional[Union[str, List[TextPart]]] = None + refusal: Optional[str] = None + tool_calls: Optional[List[MessageToolCall]] = None + + +class ToolChatMessage(BaseModel): + role: Role = Role.TOOL + tool_call_id: str + content: Union[str, List[TextPart]] + + +class DeveloperChatMessage(BaseModel): + role: Role = Role.DEVELOPER + content: Union[str, List[TextPart]] + +class ResponseChatMessage(BaseModel): + """ + Represents a response message in a conversation. + + + Args: + role: The role of the entity sending the message. + + content: The text content of the assistant message. + + refusal: A string indicating refusal reason. + + tool_calls: A list of tool call objects. + """ + role: Role = Role.ASSISTANT + content: str + refusal: Optional[str] = None + tool_calls: Optional[List[MessageToolCall]] = None + +ChatMessage = Union[SystemMessage, UserMessage, AssistantMessage, ToolChatMessage, DeveloperChatMessage, +ResponseChatMessage] + +__all__ = ["Role", "SystemMessage", "UserMessage", "AssistantMessage", "ToolChatMessage", "DeveloperChatMessage", + "ChatMessage", "ResponseChatMessage", "FunctionCall", "MessageToolCall"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/multimodal_items.py b/packages/gen/gen_ai_hub/orchestration_v2/models/multimodal_items.py new file mode 100644 index 0000000..9ae2d5e --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/multimodal_items.py @@ -0,0 +1,162 @@ +# pylint: disable=duplicate-code +""" +Models for representing multimodal content parts, including text and images. +""" + +import base64 +import mimetypes +from enum import Enum +from typing import Any, Optional, Union, Literal, Callable + +from pydantic import Field +from pydantic.main import IncEx + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class ImageDetailLevel(Enum): + """ + Controls the resolution and detail level for image analysis. + + Attributes: + AUTO: The model determines the detail level automatically. + + LOW: The model uses a low-fidelity, faster version of the image. + + HIGH: The model uses a high-fidelity version of the image. + """ + AUTO = "auto" + LOW = "low" + HIGH = "high" + + +class TextPart(BaseModel): + """ + Represents a text segment within a multimodal content block. + + Args: + text: The string content of the text part. + + type: The type identifier, defaulting to "text". + """ + text: str + type_: Literal["text"] = Field(default="text", alias="type") + + +class ImageUrl(BaseModel): + """ + A data structure holding the URL and detail level for an image. + + Args: + url: The location of the image, as a standard or data URL. + + detail: The processing detail level for the image. + """ + url: str + detail: Optional[ImageDetailLevel] = None + + +# @dataclass +class ImagePart(BaseModel): + """ + Represents an image segment within a multimodal content block. + + Args: + image_url: An `ImageUrl` object containing the image's location and detail level. + + type: The type identifier, defaulting to "image_url". + """ + image_url: ImageUrl + type_: Literal["image_url"] = Field(default="image_url", alias="type") + + +ContentPart = Union[TextPart, ImagePart] + + +class ImageItem(BaseModel): + """ + Represents an image for use in multimodal messages. + + Args: + url: The image location, specified as either a standard URL or a data URL. + - Standard URL example: 'https://example.com/image.png' + + - Data URL example: 'data:image/png;base64,...' + + detail: The image detail level for model processing. + + Example: + # Using a standard URL + img1 = ImageItem(url="https://example.com/image.png", detail=ImageDetailLevel.HIGH) + + # Using a data URL + img2 = ImageItem(url="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...") + """ + + url: Optional[str] = None + detail: Optional[ImageDetailLevel] = None + + @staticmethod + def from_file( + file_path: str, + mime_type: Optional[str] = None, + detail: Optional[ImageDetailLevel] = None, + ) -> "ImageItem": + """Create an ImageItem from a local image file. + + :param file_path: Path to the image file. + :type file_path: str + :param mime_type: Explicit MIME type (e.g., 'image/png'). + If not provided, the MIME type will be guessed from the file extension. + :type mime_type: Optional[str], optional + :param detail: The image detail level for model processing. + :type detail: Optional[ImageDetailLevel], optional + :raises ValueError: If the MIME type cannot be determined and is not provided. + :return: An ImageItem instance with the image data as a data URL. + :rtype: ImageItem + """ + + mime = mime_type or mimetypes.guess_type(file_path)[0] + if not mime: + raise ValueError( + f"Could not determine MIME type for file: {file_path}. " + "Please provide mime_type explicitly." + ) + with open(file_path, "rb") as file: + encoded = base64.b64encode(file.read()).decode("utf-8") + data_url = f"data:{mime};base64,{encoded}" + + return ImageItem(url=data_url, detail=detail) + + def model_dump( # pylint: disable=arguments-differ + self, + *, + mode: Literal['json', 'python'] | str = 'python', + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool | Literal['none', 'warn', 'error'] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + return ImagePart(image_url=ImageUrl(url=self.url, detail=self.detail)).model_dump( + mode=mode, + include=include, + exclude=exclude, + context=context, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + round_trip=round_trip, + warnings=warnings, + fallback=fallback, + serialize_as_any=serialize_as_any, + ) + +__all__ = ["ImageDetailLevel", "TextPart", "ImageUrl", "ImagePart", "ContentPart", "ImageItem"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/orchestration_request.py b/packages/gen/gen_ai_hub/orchestration_v2/models/orchestration_request.py new file mode 100644 index 0000000..664fa73 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/orchestration_request.py @@ -0,0 +1,32 @@ +from typing import List, Optional +from pydantic import model_validator + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, OrchestrationConfigReference +from gen_ai_hub.orchestration_v2.models.message import ChatMessage + +class CompletionPostRequest(BaseModel): + """ + Represents a request for the orchestration process, including configuration, + template values, and message history. + """ + config: Optional[OrchestrationConfig] = None + config_ref: Optional[OrchestrationConfigReference] = None + placeholder_values: Optional[dict[str, str]] = None + messages_history: Optional[List[ChatMessage]] = None + + @model_validator(mode="after") + def validate_config_or_ref(self): + """validates that exactly one of 'config' or 'config_ref' is provided. + + :raises ValueError: If neither or both 'config' and 'config_ref' are provided. + :return: The validated OrchestrationRequest instance. + :rtype: CompletionPostRequest + """ + if (self.config is None) == (self.config_ref is None): + raise ValueError( + "Exactly one of 'config' or 'config_ref' must be provided." + ) + return self + +__all__ = ["CompletionPostRequest"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/response.py b/packages/gen/gen_ai_hub/orchestration_v2/models/response.py new file mode 100644 index 0000000..7fb32d2 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/response.py @@ -0,0 +1,411 @@ +""" +Response models for orchestration v2 +""" + +from typing import List, Optional, Any, Literal, Union +from pydantic import ConfigDict, Field + +from gen_ai_hub.orchestration.models.response import ModuleResultsStreaming +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.message import ChatMessage, FunctionCall, ResponseChatMessage + + +class ResponseBaseModel(BaseModel): + """ + Abstract base model that extends Pydantic's BaseModel and ABC. + + - `extra="allow"` allows unexpected fields in responses to be accepted, + since the external API might introduce new attributes in the response. + + This enforces consistent and safe serialization behavior across all + derived models. + """ + + model_config = ConfigDict( + extra="allow", + frozen=False, + ) + + +class PromptTokensDetails(ResponseBaseModel): + """ + Represents the details of prompt tokens used in a specific operation. + + Attributes: + audio_tokens (Optional[int]): Audio input tokens present in the prompt. + cached_tokens (Optional[int]): Cached tokens present in the prompt. + """ + audio_tokens: Optional[int] = None + cached_tokens: Optional[int] = None + +class CompletionTokensDetails(ResponseBaseModel): + """ + Breakdown of tokens used in a completion. + + Attributes: + accepted_prediction_tokens (Optional[int]): When using Predicted Outputs, the number of tokens in the + prediction that appeared in the completion. + audio_tokens (Optional[int]): Audio input tokens generated by the model. + reasoning_tokens (Optional[int]): Tokens generated by the model for reasoning. + rejected_prediction_tokens (Optional[int]): When using Predicted Outputs, the number of tokens in the + prediction that did not appear in the completion. However, like reasoning tokens, these tokens + are still counted in the total completion tokens for purposes of billing, output, and context + window limits. + """ + accepted_prediction_tokens: Optional[int] = None + audio_tokens: Optional[int] = None + reasoning_tokens: Optional[int] = None + rejected_prediction_tokens: Optional[int] = None + +class TokenUsage(ResponseBaseModel): + """ + Usage of tokens in the response + """ + completion_tokens: int + prompt_tokens: int + total_tokens: int + prompt_tokens_details: Optional[PromptTokensDetails] = None + completion_tokens_details: Optional[CompletionTokensDetails] = None + + +class GenericModuleResult(ResponseBaseModel): + """ + Generic module result + Args: + message: Some message created from the module. Example: Input to LLM is masked successfully. + + data: Additional data object from the module + """ + message: str + data: Optional[Any] = None + + +class TopLogprob(ResponseBaseModel): + """ + Represents one of the most likely tokens and its log probability + at a given token position. + + Attributes: + token: The token. + + logprob: The log probability of this token. + + bytes: UTF-8 bytes of the token, if applicable. + """ + token: str + logprob: float + bytes: Optional[List[int]] = None + + +class ChatCompletionTokenLogprob(ResponseBaseModel): + """ + Represents a token in the message content along with its + log probability and alternative top log probabilities. + + Attributes: + token: The token. + + logprob: The log probability of this token. + + bytes: UTF-8 bytes of the token, if applicable. + + top_logprobs: List of most likely tokens and their log probabilities + at this token position. + """ + token: str + logprob: float + bytes: Optional[List[int]] = None + top_logprobs: Optional[List[TopLogprob]] = None + + +class ChoiceLogprobs(ResponseBaseModel): + """ + Log probabilities for the choice. + """ + content: Optional[List[ChatCompletionTokenLogprob]] = None + refusal: Optional[List[ChatCompletionTokenLogprob]] = None + + +class LLMChoice(ResponseBaseModel): + """ + Args: + index: Index of the choice + + message: Message from the LLM + + logprobs: Log probabilities for the choice + + finish_reason: Reason the model stopped generating tokens. + - 'stop' if the model hit a natural stop point or a provided stop sequence, + + - 'length' if the maximum token number was reached, + + - 'content_filter' if content was omitted due to a filter enforced by the LLM model provider + or the content filtering module + """ + + index: int + message: ResponseChatMessage + logprobs: Optional[ChoiceLogprobs] = None + finish_reason: str + + +class StreamFunctionObject(FunctionCall): + """ + Represents a function call with its name and arguments. + + Attributes: + name: str + The name of the function to call. + + arguments: str + The arguments to call the function with, as generated by the model in JSON + format. Note that the model does not always generate valid JSON, and may + hallucinate parameters not defined by your function schema. Validate the + arguments in your code before calling your function. + """ + model_config = ConfigDict( + extra="allow", + frozen=False, + ) + + name: Optional[str] = None + arguments: Optional[str] = None + + +class StreamToolCall(ResponseBaseModel): + type_: Literal["function"] = Field(default="function", + alias="type", + description="The type of the tool. Currently, only function is supported.") + index: int + id: Optional[str] = None + function: Optional[StreamFunctionObject] = None + + +class StreamDelta(ResponseBaseModel): + role: Optional[str] = None + content: str + tool_calls: Optional[List[StreamToolCall]] = None + + +class StreamLLMChoice(ResponseBaseModel): + index: int + delta: StreamDelta + logprobs: Optional[ChoiceLogprobs] = None + finish_reason: Optional[str] = None + + +class Citation(ResponseBaseModel): + """ + Represents a citation with related metadata. + + Attributes: + ref_id (Optional[int]): Unique identifier for inline citation + title (str): The title of the citation. + url (str): The URL of the citation. + start_index (Optional[int]): The starting index position of the citation + in a referenced text. + end_index (Optional[int]): The ending index position of the citation in + a referenced text. + """ + ref_id: Optional[int] = None + title: str + url: str + start_index: Optional[int] = None + end_index: Optional[int] = None + + +class LLMModuleResult(ResponseBaseModel): + """ + Output from LLM. Follows the OpenAI spec. + + Attributes: + id: Unique identifier for the response. + + object: Type of object returned (e.g., "chat.completion"). + + created: Unix timestamp of when the result was created. + + model: The model name (e.g., "gpt-4o-mini"). + + system_fingerprint: Optional system fingerprint associated with the result. + + choices: List of LLMChoice objects representing the output choices. + + usage: TokenUsage object representing the token usage statistics. + + citations: Optional list of citations associated with the response. + """ + id: str + object: str + created: int + model: str + system_fingerprint: Optional[str] = None + choices: List[LLMChoice] + usage: TokenUsage + citations: Optional[list[Citation]] = None + + +class StreamLLMModuleResult(LLMModuleResult): + choices: List[StreamLLMChoice] + usage: Optional[TokenUsage] = None + + +class ModuleResults(ResponseBaseModel): + """Represents the results of each module used in a processing pipeline. + + Attributes: + grounding: Optional result from the grounding module. + + templating: Optional list of chat messages resulting from the templating + module. + + input_translation: Optional result from the input translation module. + + input_masking: Optional result from the input masking module. + + input_filtering: Optional result from the input filtering module. + + output_filtering: Optional result from the output filtering module. + + output_translation: Optional result from the output translation module. + + llm: Optional result from an LLM-specific module. + + output_unmasking: Optional list of choices from the output unmasking + module. + """ + grounding: Optional[GenericModuleResult] = None + templating: Optional[List[ChatMessage]] = None + input_translation: Optional[GenericModuleResult] = None + input_masking: Optional[GenericModuleResult] = None + input_filtering: Optional[GenericModuleResult] = None + output_filtering: Optional[GenericModuleResult] = None + output_translation: Optional[GenericModuleResult] = None + llm: Optional[LLMModuleResult] = None + output_unmasking: Optional[List[LLMChoice]] = None + + +class StreamModuleResults(ModuleResults): + llm: Optional[StreamLLMModuleResult] = None + output_unmasking: Optional[List[StreamLLMChoice]] = None + + +class SAPAPIError(ResponseBaseModel): + """ + Represents an error returned from an SAP API. + + Attributes: + request_id (str): The unique identifier of the request associated + with the error. + + code (int): The http error code. + + message (str): A detailed message describing the error. + + location (str): The location where the error occurred + + intermediate_results (Optional[ModuleResults]): Optional attribute + to store any + processing results + if available or + applicable. + """ + request_id: str + code: int + message: str + location: str + intermediate_results: Optional[ModuleResults] = None + headers: Optional[dict[str, str]] = None + +class SAPAPIErrorStreaming(ResponseBaseModel): + """ + Represents an error returned from an SAP API. + + Attributes: + request_id (str): The unique identifier of the request associated + with the error. + + code (int): The http error code. + + message (str): A detailed message describing the error. + + location (str): The location where the error occurred + + intermediate_results (Optional[ModuleResults]): Optional attribute + to store any + processing results + if available or + applicable. + """ + request_id: str + code: int + message: str + location: str + intermediate_results: Optional[ModuleResultsStreaming] = None + headers: Optional[dict[str, str]] = None + +class CompletionPostResponse(ResponseBaseModel): + """ + Represents the response for a completion post request. + + Attributes: + request_id (str): Unique identifier for the completion request. + + intermediate_results (ModuleResults): Results from various modules executed during the processing. + + final_result (LLMModuleResult): Output from LLM. Follows the OpenAI spec. + """ + request_id: str + intermediate_results: ModuleResults + final_result: LLMModuleResult + intermediate_failures: Optional[List[SAPAPIError]] = None + + +class StreamCompletionPostResponse(ResponseBaseModel): + request_id: str + intermediate_results: Optional[StreamModuleResults] + final_result: Optional[StreamLLMModuleResult] + intermediate_failures: Optional[List[SAPAPIError]] = None + +class ErrorResponse(ResponseBaseModel): + error: Union[SAPAPIError, list[SAPAPIError]] + +class ErrorResponseStreaming(ResponseBaseModel): + error: Union[SAPAPIErrorStreaming, list[SAPAPIErrorStreaming]] + +class OrchestrationResponseWithRetries(CompletionPostResponse): + """ + Extended CompletionPostResponse that includes retry count information. + + This is returned when using retry-enabled methods like run_with_retries(). + + Attributes: + retries: Number of retry attempts that were made to successfully complete this request. + """ + retries: int = 0 + +__all__ = ["PromptTokensDetails", + "CompletionTokensDetails", + "TokenUsage", + "GenericModuleResult", + "TopLogprob", + "ChatCompletionTokenLogprob", + "ChoiceLogprobs", + "LLMChoice", + "StreamFunctionObject", + "StreamToolCall", + "StreamDelta", + "StreamLLMChoice", + "Citation", + "LLMModuleResult", + "StreamLLMModuleResult", + "ModuleResults", + "StreamModuleResults", + "SAPAPIError", + "SAPAPIErrorStreaming", + "CompletionPostResponse", + "StreamCompletionPostResponse", + "ErrorResponse", + "ErrorResponseStreaming", + "OrchestrationResponseWithRetries"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/response_format.py b/packages/gen/gen_ai_hub/orchestration_v2/models/response_format.py new file mode 100644 index 0000000..a84a3ff --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/response_format.py @@ -0,0 +1,94 @@ +""" +Response format models for model output specification. +""" + +import re +from enum import Enum +from typing import Optional + +from pydantic import Field, field_validator + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class ResponseFormatType(str, Enum): + """ + Enumerates the supported response format. + + Response format that the model output should adhere to. This is the same as the OpenAI definition. + + Values: + TEXT: Response format as text + + JSON_OBJECT: Response format as json object + + JSON_SCHEMA: Response format as defined json schema + """ + TEXT = "text" + JSON_OBJECT = "json_object" + JSON_SCHEMA = "json_schema" + + +class ResponseFormatText(BaseModel): + """ + Response format that the model output should adhere to. + """ + type_: ResponseFormatType = Field(default=ResponseFormatType.TEXT, alias="type") + + +class ResponseFormatJsonObject(BaseModel): + """ + Response format JSON Object that the model output should adhere to. + """ + type_: ResponseFormatType = Field(default=ResponseFormatType.JSON_OBJECT, alias="type") + + +class JSONResponseSchema(BaseModel): + """ + Response format JSON Schema that the model output should adhere to. + + Args: + name: The name of the response format. + + description: A description of what the response format is for. + + schema: A schema for the response format described as a JSON Schema object. + + strict: Whether to enable strict schema adherence when generating the output. + """ + name: str + description: Optional[str] = None + schema_: dict = Field(default_factory=dict, + alias="schema", + description="The schema for the response format, described as a JSON Schema object.") + strict: bool = False + + @field_validator("name", mode="before") + def validate_name(cls, name): # pylint: disable=no-self-argument + """validates the name of the response format. + + :param name: the name to validate + :type name: str + :raises ValueError: if the name does not match the required pattern or exceeds the maximum length + :return: the validated name + :rtype: str + """ + + pattern = r'^[a-zA-Z0-9_-]+$' + if re.match(pattern, name): + if len(name) > 64: + raise ValueError("The name of the response format must be a-z, A-Z, 0-9, " + "or contain underscores and dashes, with a maximum length of 64.") + else: + raise ValueError("The name of the response format must be a-z, A-Z, 0-9, " + "or contain underscores and dashes, with a maximum length of 64.") + + return name + + +class ResponseFormatJsonSchema(BaseModel): + type_: ResponseFormatType = Field(default=ResponseFormatType.JSON_SCHEMA, alias="type") + json_schema: JSONResponseSchema + +__all__ = ["ResponseFormatType", "ResponseFormatText", "ResponseFormatJsonObject", "ResponseFormatJsonSchema", + "JSONResponseSchema"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/streaming.py b/packages/gen/gen_ai_hub/orchestration_v2/models/streaming.py new file mode 100644 index 0000000..9fc8ddc --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/streaming.py @@ -0,0 +1,37 @@ +""" +Streaming options for content generation. +""" + +from typing import List, Optional + +from pydantic import model_validator + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class GlobalStreamOptions(BaseModel): + """ + Represents options for streaming content generation. + Args: + enabled(bool, optional): If true, the response will be streamed back to the client. + + chunk_size(int, optional): Minimum number of characters per chunk that post-LLM modules operate on. + + delimiters(list(str), optional): List of delimiters to split the input text into chunks.Please note, + this is a required parameter when input_translation_module_config or + output_translation_module_config are configured. + """ + enabled: Optional[bool] = False + chunk_size: Optional[int] = 100 + delimiters: Optional[List[str]] = None + + def model_dump(self, **kwargs): + """Override model_dump to exclude chunk_size and delimiters when enabled is False.""" + data = super().model_dump(**kwargs) + if not self.enabled: + # Remove chunk_size and delimiters from output when streaming is disabled + data.pop('chunk_size', None) + data.pop('delimiters', None) + return data + +__all__ = ["GlobalStreamOptions"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/template.py b/packages/gen/gen_ai_hub/orchestration_v2/models/template.py new file mode 100644 index 0000000..64f8129 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/template.py @@ -0,0 +1,42 @@ +""" +Defines data models for prompt templating in the orchestration module. +""" + +from typing import List, Optional + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import ChatMessage +from gen_ai_hub.orchestration_v2.models.response_format import ( + ResponseFormatText, + ResponseFormatJsonObject, + ResponseFormatJsonSchema +) +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef +from gen_ai_hub.orchestration_v2.models.tools import FunctionTool + + +class Template(BaseModel): + """ + Represents a configurable template for generating prompts or conversations. + + Args: + template: A list of prompt messages that form the template. + + defaults: A dict of default values for template variables. + + tools: A list of tool definitions. + + response_format: A response format that the model output should adhere to. + """ + template: List[ChatMessage] + defaults: Optional[dict] = None + response_format: Optional[ResponseFormatText | ResponseFormatJsonObject | ResponseFormatJsonSchema] = None + tools: Optional[List[dict | FunctionTool]] = None + + +class PromptTemplatingModuleConfig(BaseModel): + prompt: Template | TemplateRef + model: LLMModelDetails + +__all__ = ["Template", "PromptTemplatingModuleConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/template_ref.py b/packages/gen/gen_ai_hub/orchestration_v2/models/template_ref.py new file mode 100644 index 0000000..f6b79cd --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/template_ref.py @@ -0,0 +1,46 @@ +""" +Module for template reference models. +""" +from typing import Literal, Optional +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class TemplateRefByID(BaseModel): + """ + Represents a prompt template reference for generating prompts or conversations. + Args: + id(str): ID of the template in prompt registry + scope(Optional[Literal["resource_group", "tenant"]]): Defines the scope that is searched + for the referenced template. 'tenant' indicates the template is shared across all + resource groups within the tenant, while 'resource_group' indicates the template is + only accessible within the specific resource group. Defaults to 'tenant'. + """ + id: str + scope: Optional[Literal["resource_group", "tenant"]] = "tenant" + + +class TemplateRefByScenarioNameVersion(BaseModel): + """ + Represents a prompt template reference for generating prompts or conversations. + Args: + scenario(str): Scenario name + + name(str): Name of template + + version(str): Version of template + + scope(Optional[Literal["resource_group", "tenant"]]): Defines the scope that is searched + for the referenced template. 'tenant' indicates the template is shared across all + resource groups within the tenant, while 'resource_group' indicates the template is + only accessible within the specific resource group. Defaults to 'tenant'. + """ + scenario: str + name: str + version: str + scope: Optional[Literal["resource_group", "tenant"]] = "tenant" + + +class TemplateRef(BaseModel): + template_ref: TemplateRefByID | TemplateRefByScenarioNameVersion + +__all__ = ["TemplateRef", "TemplateRefByID", "TemplateRefByScenarioNameVersion"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/tools.py b/packages/gen/gen_ai_hub/orchestration_v2/models/tools.py new file mode 100644 index 0000000..db98572 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/tools.py @@ -0,0 +1,224 @@ +# pylint: disable=duplicate-code +""" +Models for tools used in chat completions with function calling. +""" + +import inspect +import typing +from typing import Any, Callable +from typing import Literal, Optional + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +def python_type_to_json_type(py_type): + """Convert a Python type to a JSON Schema type. + + :param py_type: the Python type to convert + :type py_type: any + :return: A dictionary representing the JSON Schema type. + :rtype: dict + """ + origin = typing.get_origin(py_type) + args = typing.get_args(py_type) + + # Simple types + if py_type is str: + return {"type": "string"} + if py_type in (int, float): + return {"type": "number"} + if py_type is bool: + return {"type": "boolean"} + if py_type is type(None): + return {"type": "null"} + + # List/array + if origin in (list, typing.List): + item_type = args[0] if args else str + return { + "type": "array", + "items": python_type_to_json_type(item_type) + } + + # Dict/object + if origin in (dict, typing.Dict): + value_type = args[1] if len(args) > 1 else str + return { + "type": "object", + "additionalProperties": python_type_to_json_type(value_type) + } + + # Union/Optional + if origin is typing.Union: + json_types = [python_type_to_json_type(a) for a in args] + # Handle Optional[X] (Union[X, NoneType]) + non_null_types = [t for t in json_types if t.get("type") != "null"] + if len(json_types) == 2 and len(non_null_types) == 1: + result = non_null_types[0].copy() + result["nullable"] = True + return result + return {"anyOf": json_types} + + # Fallback + return {"type": "string"} + + +class ChatCompletionTool(BaseModel): + """ + Base class for all chat completion tools. + + Args: + type (Literal["function"]): The type of the tool. Currently, only function is supported. + """ + type_: Literal["function"] = Field(default="function", + alias="type", + description="The type of the tool. Currently, only function is supported.") + + +class FunctionObject(BaseModel): + """ + Represents a function. + Args: + name (str): The name of the function to be called. Must be a-z, A-Z, 0-9, + or contain underscores and dashes, with a maximum length of 64. + + description (str): A description of what the function does, used by the model + to choose when and how to call the function. + + parameters (dict): The parameters the functions accepts, described as a JSON Schema object. + Omitting parameters defines a function with an empty parameter list. + + strict (bool, optional): Whether to enable strict schema adherence when generating the function call. + If set to true, the model will follow the exact schema defined in the parameters field. + Only a subset of JSON Schema is supported when strict is true. Defaults to False. + """ + description: Optional[str] = None + name: str + parameters: Optional[dict] + strict: bool = False + function: Optional[Callable] = Field(default=None, exclude=True) + + +class FunctionTool(ChatCompletionTool): + """ + Represents a function tool for OpenAI-like function calling. + + Args: + type (Literal["function"]): The type of the tool. Currently, only function is supported. + + function (FunctionObject): The function to be called. + """ + type_: Literal["function"] = Field(default="function", alias="type") + function: FunctionObject + + def execute(self, **kwargs: Any) -> Any: + """ + Execute the function with the provided arguments. + """ + if self.function.function is None: + raise ValueError("Function is not set.") + + if self.function.strict: + for key in kwargs.keys(): + if key not in self.function.parameters["properties"]: + raise ValueError(f"Unexpected argument '{key}' for function '{self.function.name}'") + + return self.function.function(**kwargs) + + async def aexecute(self, **kwargs: Any) -> Any: + """ + Asynchronously execute the function with the provided arguments. + """ + if self.function.function is None: + raise ValueError("Function is not set.") + + if self.function.strict: + for key in kwargs.keys(): + if key not in self.function.parameters["properties"]: + raise ValueError(f"Unexpected argument '{key}' for function '{self.function.name}'") + + return await self.function.function(**kwargs) + + @staticmethod + def from_function( + func: Callable, + *, + description: Optional[str] = None, + strict: bool = False + ) -> "FunctionTool": + """ + Create a FunctionTool from a Python function. + + Args: + func (Callable): The function to be converted to a FunctionTool. + + description (Optional[str]): A description of the function. Defaults to the docstring of the function. + + strict (bool): Whether to enable strict schema adherence when generating the function call. + """ + tool_description = description or inspect.getdoc(func) + sig = inspect.signature(func) + type_hints = typing.get_type_hints(func) + param_schema = {} + + for name, param in sig.parameters.items(): + if name not in type_hints: + raise TypeError( + f"Parameter '{name}' in '{func.__name__}' is missing a type hint." + ) + param_type = type_hints.get(name, str) + param_schema[name] = python_type_to_json_type(param_type) + + parameters = { + "type": "object", + "properties": param_schema, + "required": [ + name for name, param in sig.parameters.items() + if param.default is inspect.Parameter.empty + ], + "additionalProperties": False + } + + return FunctionTool( + function=FunctionObject( + name=func.__name__, + description=tool_description, + parameters=parameters, + strict=strict, + function=func) + ) + + +def function_tool( + func: Optional[Callable] = None, *, description: Optional[str] = None, strict: bool = False +) -> Callable[[Callable], FunctionTool] | FunctionTool: + """ + Decorator that converts a function into a FunctionTool. + + Usage: + @function_tool + def my_func(...): ... + + @function_tool() + def my_func(...): ... + """ + + def decorator(func_: Callable) -> FunctionTool: + return FunctionTool.from_function(func=func_, description=description, strict=strict) + + if func is not None and callable(func): + # Used as @function_tool + return decorator(func) + + # Used as @function_tool() + return decorator + +__all__ = [ + "python_type_to_json_type", + "ChatCompletionTool", + "FunctionObject", + "FunctionTool", + "function_tool" +] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/models/translation.py b/packages/gen/gen_ai_hub/orchestration_v2/models/translation.py new file mode 100644 index 0000000..81fc4b2 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/models/translation.py @@ -0,0 +1,114 @@ +""" +Translation module configuration models. +""" + +from enum import Enum +from typing import Optional, Literal, Union + +from pydantic import Field + +from gen_ai_hub.orchestration_v2.models.base import ABCBaseModel as BaseModel + + +class TranslationType(str, Enum): + """Enumerates supported translation types.""" + SAP_DOCUMENT_TRANSLATION = "sap_document_translation" + + +class TranslationConfig(BaseModel): + """ + Configuration for sap_document_translation translation provider. + + Args: + source_language: Language of the text to be translated. Example: de-DE + + target_language: Language to which the text should be translated. Example: en-US + """ + + source_language: Optional[str] = None + target_language: str + + +class SAPDocumentTranslation(BaseModel): + """ + Configuration for translation module. + + Args: + type: The type of translation module (e.g., 'sap_document_translation'). + + config: Configuration object for the translation module. + """ + type_: TranslationType = Field(default=TranslationType.SAP_DOCUMENT_TRANSLATION, alias="type") + config: TranslationConfig + + +class SAPDocumentTranslationApplyToSelector(BaseModel): + """ + This selector allows you to define the scope of translation, such as specific placeholders or + messages with specific roles. + For example, {"category": "placeholders", + "items": ["user_input"], + "source_language": "de-DE"} + targets the value of "user_input" in placeholder_values specified in the request payload; + and considers the value to be in German. + """ + category: Literal["placeholders", "template_roles"] + items: list[str] + source_language: str + +class InputTranslationConfig(TranslationConfig): + """ + Configuration for input translation. + + Args: + source_language: Language of the text to be translated. Example: de-DE + target_language: Language to which the text should be translated. Example: en-US + apply_to: List of selectors that define the scope of translation. + """ + apply_to: Optional[list[SAPDocumentTranslationApplyToSelector]] = None + + +class OutputTranslationConfig(TranslationConfig): + target_language: Union[str, SAPDocumentTranslationApplyToSelector] + + +class SAPDocumentTranslationInput(SAPDocumentTranslation): + """ + Configuration for input translation + + Args: + type: The type of translation module (e.g., 'sap_document_translation'). + + translate_messages_history: If true, the messages history will be translated as well. + + config: Configuration object for the translation module. + """ + translate_messages_history: Optional[bool] = None + config: Union[InputTranslationConfig, TranslationConfig] + +class SAPDocumentTranslationOutput(SAPDocumentTranslation): + """ + Configuration for output translation + + Args: + type: The type of translation module (e.g., 'sap_document_translation'). + + config: Configuration object for the translation module. + """ + config: Union[OutputTranslationConfig, TranslationConfig] + +class TranslationModuleConfig(BaseModel): + """ + Configuration for translation module + + Args: + input: Configuration for input translation + + output: Configuration for output translation + """ + input: Optional[Union[SAPDocumentTranslationInput, SAPDocumentTranslation]] = None + output: Optional[Union[SAPDocumentTranslationOutput, SAPDocumentTranslation]] = None + +__al__ = ["TranslationType", "TranslationConfig", "SAPDocumentTranslation", "SAPDocumentTranslationApplyToSelector", + "InputTranslationConfig", "OutputTranslationConfig", "SAPDocumentTranslationInput", + "SAPDocumentTranslationOutput", "TranslationModuleConfig"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/service.py b/packages/gen/gen_ai_hub/orchestration_v2/service.py new file mode 100644 index 0000000..0a4795c --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/service.py @@ -0,0 +1,816 @@ +# pylint: disable=duplicate-code +""" +Module for orchestration service handling requests and responses. + +Provides synchronous and asynchronous methods to run orchestration pipelines. +""" + +from copy import deepcopy +import random +import time +import logging +import asyncio +from functools import wraps +from typing import List, Optional, Iterable, Union + +import httpx +from ai_api_client_sdk.models.status import Status + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.orchestration_v2.models.orchestration_request import CompletionPostRequest +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, OrchestrationConfigReference +from gen_ai_hub.orchestration_v2.models.message import ChatMessage +from gen_ai_hub.orchestration_v2.models.response import (CompletionPostResponse, StreamCompletionPostResponse, + OrchestrationResponseWithRetries) +from gen_ai_hub.orchestration_v2.models.embeddings import ( + EmbeddingsOrchestrationConfig, + EmbeddingsInput, + EmbeddingsRequest, + EmbeddingsPostResponse, +) +from gen_ai_hub.orchestration_v2.sse_client import SSEClient, AsyncSSEClient, _handle_http_error +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.proxy import get_proxy_client + +V2_COMPLETION_SUFFIX = "/v2/completion" +V2_EMBEDDINGS_SUFFIX = "/v2/embeddings" +CONFIG_AND_CONFIG_REF_ERROR_TEXT = "Cannot provide both a configuration and a configuration reference." + +def cache_if_not_none(func): + """Custom cache decorator that only caches non-None results""" + cache = {} + + @wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) # Create hashable key for cache + if key not in cache: + result = func(*args, **kwargs) + if result is not None: # Only cache if result is not None + cache[key] = result + return result + return cache[key] + + def cache_clear(): + cache.clear() + + wrapper.cache_clear = cache_clear + return wrapper + + +# pylint: disable=too-many-arguments,too-many-positional-arguments +@cache_if_not_none +def discover_orchestration_api_url(base_url: str, + auth_url: str, + client_id: str, + client_secret: str, + resource_group: str, + config_id: Optional[str] = None, + config_name: Optional[str] = None, + orchestration_scenario: str = "orchestration", + executable_id: str = "orchestration") -> Optional[str]: + """Discovers the orchestration API URL based on provided configuration details. + + :param base_url: the base URL for the AI Core API. + :type base_url: str + :param auth_url: the URL for the AI Core authentication service. + :type auth_url: str + :param client_id: the client ID for the AI Core API. + :type client_id: str + :param client_secret: the client secret for the AI Core API. + :type client_secret: str + :param resource_group: the resource group for the AI Core API. + :type resource_group: str + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param orchestration_scenario: the orchestration scenario ID, defaults to "orchestration" + :type orchestration_scenario: str, optional + :param executable_id: the orchestration executable ID, defaults to "orchestration" + :type executable_id: str, optional + :return: the orchestration API URL or None if no deployment is found. + :rtype: Optional[str] + """ + + proxy_client = GenAIHubProxyClient( + base_url=base_url, + auth_url=auth_url, + client_id=client_id, + client_secret=client_secret, + resource_group=resource_group + ) + deployments = proxy_client.ai_core_client.deployment.query( + scenario_id=orchestration_scenario, + executable_ids=[executable_id], + status=Status.RUNNING + ) + if deployments.count > 0: + sorted_deployments = sorted(deployments.resources, key=lambda x: x.start_time, reverse=True) + check_for = {} + if config_name: + check_for["configuration_name"] = config_name + if config_id: + check_for["configuration_id"] = config_id + if not check_for: + return sorted_deployments[0].deployment_url + for deployment in sorted_deployments: + if all(getattr(deployment, key) == value for key, value in check_for.items()): + return deployment.deployment_url + return None + + +def get_orchestration_api_url(proxy_client: GenAIHubProxyClient, + deployment_id: Optional[str] = None, + config_name: Optional[str] = None, + config_id: Optional[str] = None) -> str: + """Retrieves the orchestration API URL based on provided deployment or configuration details. + + :param proxy_client: the GenAIHubProxyClient instance. + :type proxy_client: GenAIHubProxyClient + :param deployment_id: the deployment ID, defaults to None + :type deployment_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :raises ValueError: throws if no orchestration deployment is found. + :return: the orchestration API URL. + :rtype: str + """ + + if deployment_id: + return f"{proxy_client.ai_core_client.base_url.rstrip('/')}/inference/deployments/{deployment_id}" + url = discover_orchestration_api_url( + **proxy_client.model_dump(exclude='ai_core_client'), + config_name=config_name, + config_id=config_id + ) + if url is None: + raise ValueError('No Orchestration deployment found!') + return f"{url.rstrip('/')}" + + +class OrchestrationService: + """ + A service for executing orchestration requests, allowing for the generation of LLM-generated content + through a pipeline of configured modules. + + This service supports both synchronous and asynchronous request execution. For streaming responses, + special care is taken to not close the underlying HTTP stream prematurely. + + See https://api.sap.com/api/ORCHESTRATION_API_v2/overview + + Args: + + api_url: The base URL for the orchestration API. + + config: The default orchestration configuration. + + config_ref: The reference to default orchestration configuration. + + proxy_client: A GenAIHubProxyClient instance. + + deployment_id: Optional deployment ID. + + config_name: Optional configuration name. + + config_id: Optional configuration ID. + + timeout: Optional timeout for HTTP requests. + + + """ + + def __init__(self, + api_url: Optional[str] = None, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + proxy_client: Optional[GenAIHubProxyClient] = None, + deployment_id: Optional[str] = None, + config_name: Optional[str] = None, + config_id: Optional[str] = None, + timeout: Union[int, float, httpx.Timeout, None] = None): + """Initializes the OrchestrationService. + + :param api_url: the base URL for the orchestration API, defaults to None + :type api_url: Optional[str], optional + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + :param proxy_client: the GenAIHubProxyClient instance, defaults to None + :type proxy_client: Optional[GenAIHubProxyClient], optional + :param deployment_id: the deployment ID, defaults to None + :type deployment_id: Optional[str], optional + :param config_name: the configuration name, defaults to None + :type config_name: Optional[str], optional + :param config_id: the configuration ID, defaults to None + :type config_id: Optional[str], optional + :param timeout: the timeout for HTTP requests, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :raises ValueError: if both config and config_ref are provided. + """ + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + if api_url: + self.api_url = api_url + else: + self.api_url = get_orchestration_api_url(self.proxy_client, deployment_id, config_name, config_id) + self.config = config + self.config_ref = config_ref + if self.config_ref and self.config: + raise ValueError(CONFIG_AND_CONFIG_REF_ERROR_TEXT) + self.timeout = timeout + # create reusable httpx client to improve performance + self.client = httpx.Client(timeout=self.timeout) + self.async_client = httpx.AsyncClient(timeout=self.timeout) + + def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + # Determine the timeout to use for this request + if timeout is not None: + # Overwrite default timeout for this request + request_timeout = timeout + elif self.timeout is not None: + # Use the default timeout is set + request_timeout = self.timeout + else: + # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) + request_timeout = httpx.USE_CLIENT_DEFAULT + return request_timeout + + def _should_retry(self, error: Exception) -> bool: + """ + Determines if a request should be retried based on the error type. + + Args: + error: The exception that occurred. + + Returns: + True if the error is retryable (only 429 rate limit errors), False otherwise. + """ + if isinstance(error, httpx.HTTPStatusError): + return error.response.status_code == 429 + return False + + def _get_retry_after(self, error: Exception) -> Optional[float]: + """ + Extracts the Retry-After header value from a 429 response if available. + + Args: + error: The exception that occurred. + + Returns: + Number of seconds to wait before retrying, or None if not specified. + """ + if isinstance(error, httpx.HTTPStatusError) and error.response.status_code == 429: + retry_after = error.response.headers.get('Retry-After') + if retry_after: + try: + # Retry-After can be in seconds (integer) or HTTP date format + return float(retry_after) + except ValueError: + # If it's a date format, we'll fall back to exponential backoff + return None + return None + + def _calculate_backoff(self, retry_count: int, base_delay: float = 1.0, max_delay: float = 60.0, + min_delay: float = 0.0) -> float: + """ + Calculates exponential backoff delay with jitter. + + Uses exponential backoff capped at max_delay, with uniform random jitter + to prevent thundering herd problem when multiple clients retry simultaneously. + + Args: + retry_count: Current retry attempt number. + base_delay: Initial delay in seconds. + max_delay: Maximum delay in seconds (e.g., 60s for rate limit resets). + min_delay: Minimum delay in seconds (default: 0.0). + + Returns: + Delay in seconds before next retry, within [min_delay, max_delay] range. + """ + # Calculate exponential delay: base_delay * 2^retry_count + exp_delay = base_delay * (2 ** retry_count) + + # Cap at max_delay + capped = min(exp_delay, max_delay) + + # Ensure the lower bound doesn't exceed the cap + lower = max(0.0, min_delay) + if lower >= capped: + return capped + + # Return random value in range [lower, capped] for jitter + return random.uniform(lower, capped) + + def _execute_request( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + stream: bool = False, + ) -> Union[CompletionPostResponse | Iterable[StreamCompletionPostResponse]]: + """ + Executes an orchestration request synchronously. + + For streaming requests, this method creates a single HTTP stream. It manually enters the stream's + context to obtain the response, checks for HTTP errors, and then passes both the open response and + a custom close function to the SSE client. The SSEClient will then yield streaming events and + close the HTTP stream upon completion. + + Args: + config: The orchestration configuration. + config_ref: The orchestration configuration reference. + placeholder_values: Template values for the request. + history: History of chat messages. Can be used to provide system and assistant messages + to set the context of the conversation. Will be merged with the template message. + timeout: Optional timeout overwrite per request. + stream: Whether to stream the response. + + Returns: + A CompletionPostResponse if not streaming, or an iterable of StreamCompletionPostResponse + objects if streaming. + + Raises: + ValueError: If no configuration is provided. + OrchestrationError: If the HTTP request fails. + """ + if config is None and config_ref is None: + raise ValueError("A configuration is required to invoke the orchestration service.") + if config and config_ref: + raise ValueError(CONFIG_AND_CONFIG_REF_ERROR_TEXT) + if config and config.stream: + if config.stream.enabled != stream: + raise ValueError("Config stream setting must match used function. " + "Use stream function with config with enabled stream " + "and run function with config without enabled stream.") + + config_copy = deepcopy(config) + config_ref_copy = deepcopy(config_ref) + + request_obj = CompletionPostRequest( + config=config_copy, + config_ref=config_ref_copy, + placeholder_values=placeholder_values, + messages_history=history + ) + + if stream: + # Create the streaming response context manager. + response_cm = self.client.stream( + "POST", + self.api_url + V2_COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + return SSEClient(response_cm, prefix="data: ", final_message="[DONE]") + + response = self.client.post( + self.api_url + V2_COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return CompletionPostResponse(**data) + + async def _a_execute_request( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + stream: bool = False, + ) -> Union[CompletionPostResponse | AsyncSSEClient]: + """ + Executes an orchestration request asynchronously. + + For streaming requests, this method creates a single HTTP stream and returns an AsyncSSEClient. + The AsyncSSEClient manages the stream's lifecycle (opening it via __aenter__ and closing it via __aexit__) + and performs error checking upon entering the stream. + + Args: + config: The orchestration configuration. + config_ref: The orchestration configuration reference. + placeholder_values: Template values for the request. + history: Message history. + timeout: Optional timeout overwrite per request. + stream: Whether to stream the response. + + Returns: + A CompletionPostResponse if not streaming, or an AsyncSSEClient for iterating over the streaming response. + + Raises: + ValueError: If no configuration is provided. + OrchestrationError: If the HTTP request fails. + """ + if config is None and config_ref is None: + raise ValueError("A configuration is required to invoke the orchestration service.") + if config and config_ref: + raise ValueError(CONFIG_AND_CONFIG_REF_ERROR_TEXT) + if config and config.stream: + if config.stream.enabled != stream: + raise ValueError("Config stream setting must match used function. " + "Use astream function with config with enabled stream " + "and arun function with config without enabled stream.") + + config_copy = deepcopy(config) + config_ref_copy = deepcopy(config_ref) + + request_obj = CompletionPostRequest( + config=config_copy, + config_ref=config_ref_copy, + placeholder_values=placeholder_values, + messages_history=history, + ) + + if stream: + response_cm = self.async_client.stream( + "POST", + self.api_url + V2_COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + return AsyncSSEClient(response_cm, prefix="data: ", final_message="[DONE]") + + response = await self.async_client.post( + self.api_url + V2_COMPLETION_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return CompletionPostResponse(**data) + + def run( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> CompletionPostResponse: + """Executes an orchestration request synchronously (non-streaming). + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + if not provided, the default configuration is used. + :type config_ref: Optional[OrchestrationConfigReference], optional + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: the CompletionPostResponse object + :rtype: CompletionPostResponse + """ + + return self._execute_request( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + ) + + def stream( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> Iterable[StreamCompletionPostResponse]: + """Executes an orchestration streaming request synchronously. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + if not provided, the default configuration is used. + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: An Iterable[StreamCompletionPostResponse] object + :rtype: Iterable[StreamCompletionPostResponse] + """ + + return self._execute_request( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + stream=True + ) + + async def arun( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> CompletionPostResponse: + """Executes an orchestration request asynchronously (non-streaming). + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: the CompletionPostResponse object + :rtype: CompletionPostResponse + """ + + return await self._a_execute_request( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + ) + + async def astream( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> AsyncSSEClient: + """Executes an orchestration streaming request asynchronously. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: the AsyncSSEClient object + :rtype: AsyncSSEClient + """ + + return await self._a_execute_request( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + stream=True + ) + + def run_with_retries( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + max_retries: int = 10, + base_delay: float = 1.0, + ) -> OrchestrationResponseWithRetries | None: + """Executes an orchestration request with automatic retry on rate limits (429) and server errors. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :param max_retries: the maximum number of retry attempts, defaults to 10 + :type max_retries: int, optional + :param base_delay: the initial delay between retries in seconds, defaults to 1.0 + :type base_delay: float, optional + :return: the OrchestrationResponseWithRetries with retry count information + :rtype: OrchestrationResponseWithRetries | None + :raises ValueError: if no configuration is provided. + :raises OrchestrationError: if request fails after all retries (includes retry count). + """ + + for retry_count in range(max_retries + 1): + try: + # Execute the request + response: CompletionPostResponse = self.run( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + ) + + # Success, response with retry count + return OrchestrationResponseWithRetries( + request_id=response.request_id, # pylint: disable=no-member + intermediate_results=response.intermediate_results, # pylint: disable=no-member + final_result=response.final_result, # pylint: disable=no-member + intermediate_failures=response.intermediate_failures, + retries=retry_count, + ) + + except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + time.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) + return None + + def handle_retry(self, retry_count: int, base_delay: float, error: OrchestrationError, max_retries: int) -> float: + """ Handles retry logic with exponential backoff and jitter. + If Retry-After header exists, use it as min_delay to add jitter on top + + :param retry_count: the incremented retry attempt number + :type retry_count: int + :param base_delay: the initial delay between retries in seconds + :type base_delay: float + :param error: the exception that occurred + :type error: OrchestrationError + :param max_retries: the maximum number of retry attempts + :type max_retries: int + :raises error: throws the original error if no retry should be attempted + :return: the number of seconds to wait before next retry + :rtype: float + """ + + if not self._should_retry(error) or retry_count >= max_retries: + error.retries = retry_count + raise error + + retry_after = self._get_retry_after(error) + delay = self._calculate_backoff(retry_count, base_delay, + min_delay=0.0 if retry_after is None else retry_after) + logging.info("Retry no. %d, due to rate limiting", retry_count) + return delay + + async def arun_with_retries( + self, + config: Optional[OrchestrationConfig] = None, + config_ref: Optional[OrchestrationConfigReference] = None, + placeholder_values: Optional[dict] = None, + history: Optional[List[ChatMessage]] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + max_retries: int = 10, + base_delay: float = 1.0, + ) -> OrchestrationResponseWithRetries | None: + """Executes an orchestration request asynchronously with automatic retry on rate limits (429) and server errors. + Uses exponential backoff with jitter to handle rate limiting gracefully. + + :param config: the orchestration configuration, defaults to None + :type config: Optional[OrchestrationConfig], optional + :param config_ref: the orchestration configuration reference, defaults to None + :type config_ref: Optional[OrchestrationConfigReference], optional + :param placeholder_values: the template values, defaults to None + :type placeholder_values: Optional[dict], optional + :param history: the message history, defaults to None + :type history: Optional[List[ChatMessage]], optional + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :param max_retries: the maximum number of retry attempts, defaults to 10 + :type max_retries: int, optional + :param base_delay: the initial delay between retries in seconds, defaults to 1.0 + :type base_delay: float, optional + :return: the OrchestrationResponseWithRetries with retry count information + :rtype: OrchestrationResponseWithRetries | None + :raises ValueError: if no configuration is provided. + :raises OrchestrationError: if request fails after all retries (includes retry count). + """ + + for retry_count in range(max_retries + 1): + try: + # Execute the request + response: CompletionPostResponse = await self.arun( + config=config or self.config, + config_ref=config_ref or self.config_ref, + placeholder_values=placeholder_values, + history=history, + timeout=timeout, + ) + + return OrchestrationResponseWithRetries( + request_id=response.request_id, + intermediate_results=response.intermediate_results, + final_result=response.final_result, + intermediate_failures=response.intermediate_failures, + retries=retry_count, + ) + + except (OrchestrationError, httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException) as error: + await asyncio.sleep(self.handle_retry(retry_count, base_delay, error, max_retries)) + return None + + def embed( + self, + config: EmbeddingsOrchestrationConfig, + input: EmbeddingsInput, # pylint: disable=redefined-builtin + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> EmbeddingsPostResponse: + """Executes an embeddings request synchronously. + + :param config: the embeddings orchestration configuration + :type config: EmbeddingsOrchestrationConfig + :param input: the input text to embed + :type input: EmbeddingsInput + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: the EmbeddingsPostResponse object + :rtype: EmbeddingsPostResponse + """ + request_obj = EmbeddingsRequest( + config=deepcopy(config), + input=deepcopy(input), + ) + + response = self.client.post( + self.api_url + V2_EMBEDDINGS_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return EmbeddingsPostResponse(**data) + + async def aembed( + self, + config: EmbeddingsOrchestrationConfig, + input: EmbeddingsInput, # pylint: disable=redefined-builtin + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> EmbeddingsPostResponse: + """Executes an embeddings request asynchronously. + + :param config: the embeddings orchestration configuration + :type config: EmbeddingsOrchestrationConfig + :param input: the input text to embed + :type input: EmbeddingsInput + :param timeout: the timeout overwrite per request, defaults to None + :type timeout: Union[int, float, httpx.Timeout, None], optional + :return: the EmbeddingsPostResponse object + :rtype: EmbeddingsPostResponse + """ + request_obj = EmbeddingsRequest( + config=deepcopy(config), + input=deepcopy(input), + ) + + response = await self.async_client.post( + self.api_url + V2_EMBEDDINGS_SUFFIX, + headers=self.proxy_client.request_header, + json=request_obj.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return EmbeddingsPostResponse(**data) + + def close_http_connection(self): + """ + Closes the httpx synchronous client. + """ + self.client.close() + + async def aclose_http_connection(self): + """ + Closes the httpx asynchronous client. + """ + await self.async_client.aclose() + + +__all__ = ["OrchestrationService"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py b/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py new file mode 100644 index 0000000..5bf18ab --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/sse_client.py @@ -0,0 +1,339 @@ +# pylint: disable=duplicate-code +""" +Module for Server-Sent Events (SSE) clients for orchestration responses. + +This module provides both synchronous and asynchronous SSE clients for iterating over streaming responses. +Each client is responsible for handling HTTP errors and for closing the underlying HTTP stream +when iteration is complete. +""" + +import json +from typing import Iterable, Iterator, AsyncIterator + +import httpx + +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError, OrchestrationErrorList +from gen_ai_hub.orchestration_v2.models.response import StreamCompletionPostResponse + + +def _parse_event_data(event_data: str, final_message: str) -> "StreamCompletionPostResponse": + """ + Parses the event data JSON string into a StreamCompletionPostResponse object. + + Args: + event_data: The JSON string containing event data. + final_message: A message indicating the end of the stream. + + Returns: + An OStreamCompletionPostResponse object parsed from the event data. + Returns None if the event_data equals the final_message. + + Raises: + OrchestrationError: If the event data contains an error code. + """ + if event_data == final_message: + return None + event = json.loads(event_data) + if "error" in event: + error_event = event["error"] + if isinstance(error_event, dict): + raise OrchestrationError( + request_id=error_event.get("request_id"), + headers=httpx.Headers({}), + message=error_event.get("message"), + code=error_event.get("code"), + location=error_event.get("location"), + intermediate_results=error_event.get("intermediate_results", {}), + ) + if isinstance(error_event, list): + errors = [ + OrchestrationError( + request_id=e.get("request_id"), + headers=httpx.Headers({}), + message=e.get("message"), + code=e.get("code"), + location=e.get("location"), + intermediate_results=e.get("intermediate_results", {}), + ) + for e in error_event + if isinstance(e, dict) + ] + raise OrchestrationErrorList(errors=errors) + return StreamCompletionPostResponse(**event) + + +class SSEClient: + """ + A synchronous Server-Sent Events (SSE) client that wraps an httpx.Response for iterating + over streaming responses. + + This client reads data chunks from the HTTP stream and parses each SSE event. + For performance reasons the underlying HTTP stream is reused for subsequent calls. + """ + + def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): + """Initializes the SSEClient. + + :param response_cm: An httpx.Response context manager for the streaming response. + :type response_cm: httpx.Response + :param prefix: The prefix string that identifies SSE event data, defaults to data: + :type prefix: str, optional + :param final_message: The message that indicates the end of the stream, defaults to [DONE] + :type final_message: str, optional + """ + + self.response_cm = response_cm + self.event_prefix = prefix + self.final_message = final_message + self._response = None + self._iterator = None + + def __enter__(self): + """Synchronously enters the context for the streaming response. + + It awaits the response, checks for HTTP errors, and if an error occurs, + reads the content and raises an OrchestrationError. + + :return: Self, with the streaming response stored. + :rtype: SSEClient + """ + + self._response = self.response_cm.__enter__() + try: + self._response.raise_for_status() + except httpx.HTTPStatusError as error: + content = self._response.read() + error_response = httpx.Response( + status_code=self._response.status_code, + headers=self._response.headers, + content=content, + request=self._response.request, + ) + self.response_cm.__exit__(None, None, None) + _handle_http_error(error, error_response) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Synchronously exits the context, ensuring that the context manager is properly closed. + """ + self.response_cm.__exit__(exc_type, exc_val, exc_tb) + + def iter_lines(self) -> Iterable[str]: + """Reads data chunks from the HTTP stream and yields complete lines. + + This method accumulates incoming chunks until a newline is encountered, yielding one complete + line at a time. + + :return: Complete lines of text from the streaming response. + :rtype: Iterable[str] + :yield: Complete lines of text from the streaming response. + :rtype: Iterator[Iterable[str]] + """ + + buffer = "" + for chunk in self._response.iter_text(): + buffer += chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + yield line.strip() + if buffer: + yield buffer.strip() + + def __iter__(self) -> Iterator: + """ + Returns self as an iterator. Opens the HTTP stream and initializes the internal iterator. + """ + return self + + def __next__(self): + """ + Retrieves the next parsed SSE event from the stream. + It skips any lines that do not start with the expected prefix. When the final message is encountered + or the stream is exhausted, it closes the stream and raises StopIteration. + """ + if self._iterator is None: + self.__enter__() + self._iterator = self.iter_lines() + while True: + try: + line = next(self._iterator) + except StopIteration: + # End of stream; ensure resources are cleaned up. + self.__exit__(None, None, None) + raise StopIteration + + if not line or not line.startswith(self.event_prefix): + continue + + event_data = line[len(self.event_prefix):] + result = _parse_event_data(event_data, self.final_message) + if result is None: + # Final message encountered; close the stream. + self.__exit__(None, None, None) + raise StopIteration + return result + + +class AsyncSSEClient: + """ + An asynchronous SSE client for iterating over streaming responses. + + This client wraps an asynchronous HTTP stream (provided as a context manager) and ensures + that the stream is properly opened and closed. It also checks for HTTP errors upon entering the stream. + """ + + def __init__(self, response_cm, prefix: str = "data: ", final_message: str = "[DONE]"): + """Initializes the AsyncSSEClient. + + :param response_cm: An asynchronous context manager for the HTTP streaming response. + :type response_cm: typing.AsyncContextManager[httpx.Response] + :param prefix: the SSE data prefix, defaults to "data: " + :type prefix: str, optional + :param final_message: the message indicating the end of the stream, defaults to "[DONE]" + :type final_message: str, optional + """ + + self.response_cm = response_cm + self.event_prefix = prefix + self.final_message = final_message + self._response = None + self._iterator = None + + async def __aenter__(self): + """ + Asynchronously enters the context for the streaming response. + + It awaits the response, checks for HTTP errors, and if an error occurs, + reads the content and raises an OrchestrationError. + + Returns: + Self, with the streaming response stored. + """ + self._response = await self.response_cm.__aenter__() + try: + self._response.raise_for_status() + except httpx.HTTPStatusError as error: + content = await self._response.aread() + error_response = httpx.Response( + status_code=self._response.status_code, + headers=self._response.headers, + content=content, + request=self._response.request, + ) + await self.response_cm.__aexit__(None, None, None) + _handle_http_error(error, error_response) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """ + Asynchronously exits the context, ensuring that the context manager is properly closed. + """ + await self.response_cm.__aexit__(exc_type, exc_val, exc_tb) + + def _process_line(self, line: str) -> "StreamCompletionPostResponse": + """ + Process a single line and return parsed event data if valid. + + :param line: The line to process + :type line: str + :return: Parsed event data or None if line is invalid or end of stream + :rtype: StreamCompletionPostResponse or None + """ + line = line.strip() + if not line or not line.startswith(self.event_prefix): + return None + event_data = line[len(self.event_prefix):] + return _parse_event_data(event_data, self.final_message) + + async def _internal_iterator(self) -> AsyncIterator: + """ + Internal asynchronous generator that yields parsed events from the HTTP stream. + """ + buffer = "" + async for chunk in self._response.aiter_text(): + buffer += chunk + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + result = self._process_line(line) + if result is None: + if line.strip() == self.final_message or line.strip().endswith(self.final_message): + return + continue + yield result + # Process any remaining data in the buffer + if buffer: + result = self._process_line(buffer) + if result is not None: + yield result + + def __aiter__(self): + """ + Returns the async iterator (self). The initialization of the stream is deferred until the first + call to __anext__. + """ + return self + + async def __anext__(self): + """ + Asynchronously retrieves the next event from the stream. On the first call, it enters the asynchronous + context to start the stream. When the stream is exhausted or the final message is received, it properly + exits the context. + + Returns: + The next parsed event from the stream. + + Raises: + StopAsyncIteration: When the stream is exhausted. + """ + if self._iterator is None: + # Lazily initialize the stream. + await self.__aenter__() + self._iterator = self._internal_iterator().__aiter__() + try: + return await self._iterator.__anext__() + except StopAsyncIteration: + await self.__aexit__(None, None, None) + raise StopAsyncIteration + + +def _handle_http_error(error, response: httpx.Response): + """ + Handles HTTP errors by raising an OrchestrationError with details from the response. + + Args: + error: The original HTTP error. + response: The httpx.Response object containing error details incl. headers. + + Raises: + OrchestrationError with information extracted from the response. + """ + if not response.content: + raise error + try: + error_content = response.json().get("error", None) + except ValueError as exc: + raise error from exc + if isinstance(error_content, dict): + raise OrchestrationError( + request_id=error_content.get("request_id"), + headers=response.headers, + message=error_content.get("message"), + code=error_content.get("code"), + location=error_content.get("location"), + intermediate_results=error_content.get("intermediate_results", {}), + ) from error + if isinstance(error_content, list): + errors = [ + OrchestrationError( + request_id=e.get("request_id"), + headers=response.headers, + message=e.get("message"), + code=e.get("code"), + location=e.get("location"), + intermediate_results=e.get("intermediate_results", {}), + ) + for e in error_content + if isinstance(e, dict) + ] + raise OrchestrationErrorList(errors=errors) from error diff --git a/packages/gen/gen_ai_hub/orchestration_v2/utils.py b/packages/gen/gen_ai_hub/orchestration_v2/utils.py new file mode 100644 index 0000000..09834c1 --- /dev/null +++ b/packages/gen/gen_ai_hub/orchestration_v2/utils.py @@ -0,0 +1,11 @@ +def load_text_file(file_path): + """Loads and returns the content of a text file. + + :param file_path: The path to the text file to be loaded. + :type file_path: str + :return: The content of the file as a string. + :rtype: str + """ + + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() diff --git a/packages/gen/gen_ai_hub/prompt_registry/__init__.py b/packages/gen/gen_ai_hub/prompt_registry/__init__.py new file mode 100644 index 0000000..a03f341 --- /dev/null +++ b/packages/gen/gen_ai_hub/prompt_registry/__init__.py @@ -0,0 +1,9 @@ +from .models import * +from .client import PromptTemplateClient, OrchestrationConfigClient + +__all__ = ["PromptTemplateClient", "OrchestrationConfigClient", 'PromptTemplate', 'PromptTemplateSpec', + 'PromptTemplatePostRequest', 'PromptTemplateSubstitutionRequest', + 'PromptTemplateSubstitutionResponse', 'PromptTemplateGetResponse', 'PromptTemplatePostResponse', + 'PromptTemplateDeleteResponse', 'PromptTemplateListResponse', 'OrchestrationConfigPostRequest', + 'OrchestrationConfigPostResponse', 'OrchestrationConfigGetResponse', 'OrchestrationConfigListResponse', + 'OrchestrationConfigDeleteResponse'] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/prompt_registry/client.py b/packages/gen/gen_ai_hub/prompt_registry/client.py new file mode 100644 index 0000000..19d21d6 --- /dev/null +++ b/packages/gen/gen_ai_hub/prompt_registry/client.py @@ -0,0 +1,379 @@ +from typing import Optional +from abc import ABC +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubRestClient +from .models.prompt_template import ( + PromptTemplatePostRequest, + PromptTemplatePostResponse, + PromptTemplateGetResponse, + PromptTemplateListResponse, + PromptTemplateDeleteResponse, + PromptTemplateSubstitutionRequest, + PromptTemplateSubstitutionResponse, + PromptTemplateSpec +) +from .models.orchestration_config import ( + OrchestrationConfigPostRequest, + OrchestrationConfigPostResponse, + OrchestrationConfigGetResponse, + OrchestrationConfigListResponse, + OrchestrationConfigDeleteResponse, + OrchestrationConfig) + +# Constants +PATH_SCENARIOS = "/lm/scenarios" +PATH_PROMPT_TEMPLATES = "/lm/promptTemplates" +CONTENT_TYPE_JSON_ = "application/json" +PATH_REGISTRY_CONFIG = "/registry/v2/orchestrationConfigs" +PATH_REGISTRY_SCENARIOS = "/registry/v2/scenarios" + +class PromptRegistryClient(ABC): + """ + Client for interacting with the Prompt Registry API. + + https://api.sap.com/api/PROMPT_REGISTRY_API/overview + """ + def __init__(self, proxy_client: Optional[GenAIHubProxyClient] = None): + """Initializes the PromptRegistryClient. + + :param proxy_client: Optional proxy client to use for requests. + :type proxy_client: Optional[GenAIHubProxyClient], optional + """ + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + self.rest_client = GenAIHubRestClient(self.proxy_client) + + + +class PromptTemplateClient(PromptRegistryClient): + """ + Client for interacting with the Prompt Registry Prompt Template API. + + https://api.sap.com/api/PROMPT_REGISTRY_API/overview + """ + + def create_prompt_template(self, name: str, version: str, scenario: str, + prompt_template_spec: PromptTemplateSpec) -> PromptTemplatePostResponse: + """Create or update a prompt template. + + :param name: the name of the prompt template. + :type name: str + :param version: the version of the prompt template. + :type version: str + :param scenario: the scenario name of the prompt template. + :type scenario: str + :param prompt_template_spec: the specification of the prompt template. + :type prompt_template_spec: PromptTemplateSpec + :return: A PromptTemplatePostResponse object. + :rtype: PromptTemplatePostResponse + """ + request = PromptTemplatePostRequest(scenario=scenario, name=name, version=version, spec=prompt_template_spec) + response = self.rest_client.post(path=PATH_PROMPT_TEMPLATES, + body=request.model_dump(by_alias=True, exclude_none=True), + convert_body_to_camel_case=False) + + return PromptTemplatePostResponse(**response) + + def get_prompt_templates(self, scenario: str, name: str, version: str, retrieve: str = None, + include_spec: bool = None) -> PromptTemplateListResponse: + """Retrieve the latest version of every prompt template based on the filters. + + :param scenario: the scenario name of the prompt template. + :type scenario: str + :param name: the name of the prompt template. + :type name: str + :param version: the version of the prompt template. + :type version: str + :param retrieve: both(default), imperative, declarative + :type retrieve: str, optional + :param include_spec: false(default), true + :type include_spec: bool, optional + :return: A PromptTemplateListResponse object. + :rtype: PromptTemplateListResponse + """ + query_params = { + "scenario": scenario, + "name": name, + "version": version, + "retrieve": retrieve, + "include_spec": include_spec + } + + response = self.rest_client.get(path=PATH_PROMPT_TEMPLATES, params=query_params) + + return PromptTemplateListResponse(**response) + + def get_prompt_template_by_id(self, template_id: str) -> PromptTemplateGetResponse: + """Retrieve a specific version of the prompt template by ID. + + :param template_id: The ID of the prompt template to retrieve. + :type template_id: str + :return: A PromptTemplateGetResponse object. + :rtype: PromptTemplateGetResponse + """ + + response = self.rest_client.get(path=f"{PATH_PROMPT_TEMPLATES}/{template_id}") + + return PromptTemplateGetResponse(**response) + + def get_prompt_template_history(self, scenario: str, name: str, version: str) -> PromptTemplateListResponse: + """Retrieve the history of edits to the prompt template. Only for imperative managed prompt templates. + + :param scenario: The scenario name of the prompt template. + :type scenario: str + :param name: The name of the prompt template. + :type name: str + :param version: The version ID of the prompt template. + :type version: str + :return: A PromptTemplateListResponse object. + :rtype: PromptTemplateListResponse + """ + + response = self.rest_client.get(f"{PATH_SCENARIOS}/{scenario}/promptTemplates/{name}/versions/{version}/history") + + return PromptTemplateListResponse(**response) + + def delete_prompt_template_by_id(self, template_id: str) -> PromptTemplateDeleteResponse: + """Delete a specific version of the prompt template by ID. + + :param template_id: The ID of the prompt template to delete. + :type template_id: str + :return: A PromptTemplateDeleteResponse object. + :rtype: PromptTemplateDeleteResponse + """ + + response = self.rest_client.delete(f"{PATH_PROMPT_TEMPLATES}/{template_id}") + + return PromptTemplateDeleteResponse(**response) + + def import_prompt_template(self, file: bytes) -> PromptTemplatePostResponse: + """Import a runtime/declarative prompt template into the design time environment. + + :param file: binary file content + :type file: bytes + :return: A PromptTemplatePostResponse object. + :rtype: PromptTemplatePostResponse + """ + + # Content-Type: multipart/form-data is added automatically by requests when a file is passed in the request. + kwargs = {"files": {"file": file}} + response = self.rest_client.post(path=f"{PATH_PROMPT_TEMPLATES}/import", **kwargs) + return PromptTemplatePostResponse(**response) + + def export_prompt_template(self, template_id: str) -> bytes: + """Export a design time template in a declarative compatible yaml file. Supports only single file export. + + :param template_id: The id of the prompt template to export. + :type template_id: str + :return: bytes: The content of the exported file + :rtype: bytes + """ + + response = self.rest_client.get(path=f"{PATH_PROMPT_TEMPLATES}/{template_id}/export", return_bytes_content=True) + + return response + + def fill_prompt_template(self, scenario: str, name: str, version: str, input_params: dict, + metadata: bool = False) -> PromptTemplateSubstitutionResponse: + """Replace the placeholders of the prompt template referenced via scenario-name-version + with user provided values. + + :param scenario: the scenario name of the prompt template. + :type scenario: str + :param name: the name of the prompt template. + :type name: str + :param version: the version of the prompt template. + :type version: str + :param input_params: User provided values to replace the placeholders of the prompt template. + :type input_params: dict + :param metadata: False(default), True return resource object with all details. + :type metadata: bool, optional + :return: A PromptTemplateSubstitutionResponse object. + :rtype: PromptTemplateSubstitutionResponse + """ + + request =PromptTemplateSubstitutionRequest(input_params=input_params) + kwargs = {'convert_body_to_camel_case': False} + if metadata: + kwargs.update({'params': {"metadata": metadata}}) + response = self.rest_client.post(path=(f"{PATH_SCENARIOS}/{scenario}/promptTemplates/{name}/versions/" + f"{version}/substitution"), + headers={"Content-Type": CONTENT_TYPE_JSON_}, + body=request.model_dump(by_alias=True), + **kwargs) + + return PromptTemplateSubstitutionResponse(**response) + + def fill_prompt_template_by_id(self, + template_id: str, + input_params: dict, + metadata: bool = False, ) -> PromptTemplateSubstitutionResponse: + """Replace the placeholders of the prompt template referenced via template_id with user provided values. + + :param template_id: The ID of the prompt template. + :type template_id: str + :param input_params: User provided values to replace the placeholders of the prompt template. + :type input_params: dict + :param metadata: False(default), True return resource object with all details. + :type metadata: bool, optional + :return: A PromptTemplateSubstitutionResponse object. + :rtype: PromptTemplateSubstitutionResponse + """ + + request =PromptTemplateSubstitutionRequest(input_params=input_params) + kwargs = {'params': {"metadata": metadata}, 'convert_body_to_camel_case': False} + response = self.rest_client.post(path=f"{PATH_PROMPT_TEMPLATES}/{template_id}/substitution", + headers={"Content-Type": CONTENT_TYPE_JSON_}, + body=request.model_dump(by_alias=True), + **kwargs) + + return PromptTemplateSubstitutionResponse(**response) + +class OrchestrationConfigClient(PromptRegistryClient): + """ + Client for interacting with the Prompt Registry Orchestration Config API. + + https://api.sap.com/api/PROMPT_REGISTRY_API/overview + """ + def create_orchestration_config(self, name: str, version: str, scenario: str, + spec: OrchestrationConfig | dict) -> OrchestrationConfigPostResponse: + """Create an orchestration config. + + :param name: the name of the orchestration config. + :type name: str + :param version: the version of the orchestration config. + :type version: str + :param scenario: the scenario name of the orchestration config. + :type scenario: str + :param spec: the specification of the orchestration config. + :type spec: Union[dict, OrchestrationConfig] + + :return: An OrchestrationConfigPostResponse object. + :rtype: OrchestrationConfigPostResponse + """ + request = OrchestrationConfigPostRequest(name=name, version=version, scenario=scenario, spec=spec) + response = self.rest_client.post(path=PATH_REGISTRY_CONFIG, + body=request.model_dump(by_alias=True, exclude_none=True), + convert_body_to_camel_case=False) + + return OrchestrationConfigPostResponse(**response) + + def get_orchestration_configs( + self, scenario: str, name: str, version: str, retrieve: str = None, include_spec: bool = None, + resolve_template_ref:bool = None) -> OrchestrationConfigListResponse: + """Retrieve the latest version of every orchestration config based on the filters. + + :param scenario: the scenario name of the orchestration config. + :type scenario: str + :param name: the name of the orchestration config. + :type name: str + :param version: the version of the orchestration config. + :type version: str + :param retrieve: both(default), imperative, declarative + :type retrieve: str, optional + :param include_spec: false(default), true + :type include_spec: bool, optional + :param resolve_template_ref: false(default), true + :type resolve_template_ref: bool, optional + + :return: An OrchestrationConfigListResponse object. + :rtype: OrchestrationConfigListResponse + """ + query_params = { + "scenario": scenario, + "name": name, + "version": version, + "retrieve": retrieve, + "include_spec": include_spec, + "resolve_template_ref": resolve_template_ref + } + + response = self.rest_client.get(path=PATH_REGISTRY_CONFIG, params=query_params, + convert_params_to_camel_case=False) + + return OrchestrationConfigListResponse(**response) + + def get_orchestration_config_by_id(self, config_id: str, resolve_template_ref: bool = None + ) -> OrchestrationConfigGetResponse: + """Retrieve a specific version of the orchestration config by ID. + + :param config_id: The ID of the orchestration config to retrieve. + :type config_id: str + :param resolve_template_ref: false(default), true + :type resolve_template_ref: bool, optional + + :return: An OrchestrationConfigGetResponse object. + :rtype: OrchestrationConfigGetResponse""" + + query_params = {"resolve_template_ref": resolve_template_ref} + response = self.rest_client.get(path=f"{PATH_REGISTRY_CONFIG}/{config_id}", params=query_params, + convert_params_to_camel_case=False) + return OrchestrationConfigGetResponse(**response) + + def get_orchestration_config_history(self, scenario: str, name: str, version: str, include_spec: bool = None, + resolve_template_ref:bool = None) -> OrchestrationConfigListResponse: + """Retrieve the history of edits to the orchestration config. + + :param scenario: The scenario name of the orchestration config. + :type scenario: str + :param name: The name of the orchestration config. + :type name: str + :param version: The version ID of the orchestration config. + :type version: str + :param include_spec: false(default), true + :type include_spec: bool, optional + :param resolve_template_ref: false(default), true + :type resolve_template_ref: bool, optional + + :return: An OrchestrationConfigListResponse object. + :rtype: OrchestrationConfigListResponse + """ + response = self.rest_client.get( + path=f"{PATH_REGISTRY_SCENARIOS}/{scenario}/orchestrationConfigs/{name}/versions/{version}/history", + params={"include_spec": include_spec, "resolve_template_ref": resolve_template_ref}, + convert_params_to_camel_case=False + ) + return OrchestrationConfigListResponse(**response) + + def delete_orchestration_config_by_id(self, config_id: str) -> OrchestrationConfigDeleteResponse: + """Delete a specific version of the orchestration config by ID. + + :param config_id: The ID of the orchestration config. + :type config_id: str + + :return: An OrchestrationConfigDeleteResponse object. + :rtype: OrchestrationConfigDeleteResponse + """ + + response = self.rest_client.delete(f"{PATH_REGISTRY_CONFIG}/{config_id}") + return OrchestrationConfigDeleteResponse(**response) + + def import_orchestration_config(self, file: bytes) -> OrchestrationConfigPostResponse: + """Import a runtime/declarative orchestration config into the design time environment. + + :param file: binary file content + :type file: bytes + :return: A OrchestrationConfigPostResponse object. + :rtype: OrchestrationConfigPostResponse + """ + + # Content-Type: multipart/form-data is added automatically by requests when a file is passed in the request. + kwargs = {"files": {"file": file}} + response = self.rest_client.post(path=f"{PATH_REGISTRY_CONFIG}/import", **kwargs) + return OrchestrationConfigPostResponse(**response) + + def export_orchestration_config(self, config_id: str) -> bytes: + """Export a design orchestration config in a declarative compatible yaml file. + Supports only single file export. + + :param config_id: The id of the orchestration config to export. + :type config_id: str + :return: bytes: The content of the exported file + :rtype: bytes + """ + + response = self.rest_client.get(path=f"{PATH_REGISTRY_CONFIG}/{config_id}/export", return_bytes_content=True) + + return response + +__all__ = ["PromptTemplateClient", "OrchestrationConfigClient"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/prompt_registry/models/__init__.py b/packages/gen/gen_ai_hub/prompt_registry/models/__init__.py new file mode 100644 index 0000000..afe2fcc --- /dev/null +++ b/packages/gen/gen_ai_hub/prompt_registry/models/__init__.py @@ -0,0 +1,13 @@ +from .prompt_template import (PromptTemplate, PromptTemplateSpec, PromptTemplatePostRequest, + PromptTemplateSubstitutionRequest, PromptTemplateSubstitutionResponse, + PromptTemplateGetResponse, PromptTemplatePostResponse, PromptTemplateDeleteResponse, + PromptTemplateListResponse) +from .orchestration_config import (OrchestrationConfigPostRequest, OrchestrationConfigPostResponse, + OrchestrationConfigGetResponse, OrchestrationConfigListResponse, + OrchestrationConfigDeleteResponse) + +__all__ = ['PromptTemplate', 'PromptTemplateSpec', 'PromptTemplatePostRequest', 'PromptTemplateSubstitutionRequest', + 'PromptTemplateSubstitutionResponse', 'PromptTemplateGetResponse', 'PromptTemplatePostResponse', + 'PromptTemplateDeleteResponse', 'PromptTemplateListResponse', 'OrchestrationConfigPostRequest', + 'OrchestrationConfigPostResponse', 'OrchestrationConfigGetResponse', 'OrchestrationConfigListResponse', + 'OrchestrationConfigDeleteResponse'] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/prompt_registry/models/orchestration_config.py b/packages/gen/gen_ai_hub/prompt_registry/models/orchestration_config.py new file mode 100644 index 0000000..b1803c1 --- /dev/null +++ b/packages/gen/gen_ai_hub/prompt_registry/models/orchestration_config.py @@ -0,0 +1,107 @@ +from typing import List, Optional +from pydantic import BaseModel, Field + + +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig + + +class OrchestrationConfigPostRequest(BaseModel): + """ + Request to create an orchestration config. + + Args: + name: The name of the orchestration config. + version: The version of the orchestration config. + scenario: The scenario of the orchestration config. + spec: The orchestration config specification. + """ + name: str = Field(max_length=120) + version: str = Field(max_length=10) + scenario: str = Field(max_length=120) + spec: OrchestrationConfig + + def model_dump(self, **kwargs): + """Dumps the model to a dictionary with default settings.""" + kwargs.setdefault("by_alias", True) + kwargs.setdefault("exclude_none", True) + return super().model_dump(**kwargs) + + +class OrchestrationConfigPostResponse(BaseModel): + """ + Response to the orchestration config post request. + + Args: + message: Response message. + id: UUID of the created/updated config. + scenario: The scenario name. + name: The config name. + version: The config version. + """ + message: str + id: str + scenario: str + name: str + version: str + + +class OrchestrationConfigGetResponse(BaseModel): + """ + Response to a get orchestration config request. + + Args: + id: UUID of the config. + name: Config name. + version: Config version. + scenario: Scenario name. + creation_timestamp: When the config was created. + managed_by: Who manages the config. + is_version_head: Whether this is the head version. + spec: The orchestration config specification (optional). + """ + id: Optional[str] = None + name: Optional[str] = None + version: Optional[str] = None + scenario: Optional[str] = None + creation_timestamp: Optional[str] = None + managed_by: Optional[str] = None + is_version_head: Optional[bool] = None + resource_group_id: Optional[str] = None + spec: Optional[OrchestrationConfig] = None + + def model_dump(self, **kwargs): + """Dumps the model to a dictionary with default settings.""" + kwargs.setdefault("by_alias", True) + kwargs.setdefault("exclude_none", True) + kwargs.setdefault("exclude_unset", True) + return super().model_dump(**kwargs) + + + +class OrchestrationConfigListResponse(BaseModel): + """ + Response to list orchestration configs request. + + Args: + count: Number of configs returned. + resources: List of OrchestrationConfigGetResponse objects. + """ + count: int + resources: List[OrchestrationConfigGetResponse] + + def model_dump(self, **kwargs): + """Dumps the model to a dictionary with default settings.""" + kwargs.setdefault("by_alias", True) + kwargs.setdefault("exclude_none", True) + kwargs.setdefault("exclude_unset", True) + return super().model_dump(**kwargs) + + +class OrchestrationConfigDeleteResponse(BaseModel): + """ + Response to a delete orchestration config request. + + Args: + message: Response message. + """ + message: str diff --git a/packages/gen/gen_ai_hub/prompt_registry/models/prompt_template.py b/packages/gen/gen_ai_hub/prompt_registry/models/prompt_template.py new file mode 100644 index 0000000..bfa61c8 --- /dev/null +++ b/packages/gen/gen_ai_hub/prompt_registry/models/prompt_template.py @@ -0,0 +1,215 @@ +from typing import List, Optional, Dict, Any +from pydantic.config import ConfigDict + +from pydantic import BaseModel, Field, field_validator +from gen_ai_hub.orchestration_v2 import (ResponseFormatText, ResponseFormatJsonObject, ResponseFormatJsonSchema, + FunctionTool, ImagePart, TextPart, ImageItem, ImageUrl, ContentPart) + + +class PromptTemplate(BaseModel): + """ + Represents a prompt template. + + Args: + role: The role of the prompt template. + + content: The content of the prompt template. + """ + + role: str + """The role of the prompt template.""" + content: str | List[str| ContentPart| ImageItem] + """The content of the prompt template.""" + + @field_validator("content", mode="before") + def content_validation(cls, content): # pylint: disable=no-self-argument + """ + Validates and maps the content field to the appropriate types. + """ + + mapped_content = [] + + if isinstance(content, str): + mapped_content = content + elif isinstance(content, list): + for item in content: + if isinstance(item, (ContentPart, dict)): + mapped_content.append(item) + elif isinstance(item, str): + mapped_content.append(TextPart(text=item)) + elif isinstance(item, ImageItem): + mapped_content.append(ImagePart(image_url=ImageUrl(url=item.url, detail=item.detail))) + else: + raise ValueError("Prompt template content list must contain only " + "str, ImageItem, TextPart, or ImagePart objects") + else: + raise ValueError("Prompt template content must be a str or list of " + "str, ImageItem, TextPart, or ImagePart objects") + return mapped_content + + + +class PromptTemplateSpec(BaseModel): + """ + Represents a prompt template specification. + + Args: + Args: + template: A list of prompt messages that form the template. + + defaults: A dict of default values for template variables. + + tools: A list of tool definitions. + + response_format: A response format that the model output should adhere to. + + additional_fields: Additional fields for the prompt template. + """ + template: List[PromptTemplate] + defaults: Optional[dict] = None + response_format: Optional[ResponseFormatText | ResponseFormatJsonObject | ResponseFormatJsonSchema] = None + tools: Optional[List[dict | FunctionTool]] = None + additional_fields: Optional[Dict[Any, Any]] = Field(default_factory=dict) + + +class PromptTemplatePostRequest(BaseModel): + """ + Represents a request to create a prompt template. + + Args: + name: The name of the prompt template. + + version: The version of the prompt template. + + scenario: The scenario of the prompt template. + + spec: The specification of the prompt template. + """ + name: str + """The name of the prompt template.""" + version: str + """The version of the prompt template.""" + scenario: str + """The scenario of the prompt template.""" + spec: PromptTemplateSpec + """The specification of the prompt template.""" + + +class PromptTemplatePostResponse(BaseModel): + """ + Represents a response to a request to create a prompt template. + + Args: + message: The message of the response. + + id: The ID of the prompt template. + + scenario: The scenario of the prompt template. + + name: The name of the prompt template. + + version: The version of the prompt template. + """ + message: str + """The message of the response.""" + id: str + """The ID of the prompt template.""" + scenario: str + """The scenario of the prompt template.""" + name: str + """The name of the prompt template.""" + version: str + """The version of the prompt template.""" + + +class PromptTemplateGetResponse(BaseModel): + """ + Represents a response to a request to get a prompt template. + + Args: + id: The ID of the prompt template. + + name: The name of the prompt template. + + version: The version of the prompt template. + + scenario: The scenario of the prompt template. + + creation_timestamp: The creation timestamp of the prompt template. + + managed_by: The manager of the prompt template. + + is_version_head: Whether the version is the head version. + + spec: The specification of the prompt template. + """ + id: str + """The ID of the prompt template.""" + name: str + """The name of the prompt template.""" + version: str + """The version of the prompt template.""" + scenario: str + """The scenario of the prompt template.""" + creation_timestamp: Optional[str] = None + """The creation timestamp of the prompt template.""" + managed_by: Optional[str] = None + """The manager of the prompt template.""" + is_version_head: Optional[bool] = None + """Whether the version is the head version.""" + spec: Optional[PromptTemplateSpec] = None + """The specification of the prompt template.""" + + +class PromptTemplateListResponse(BaseModel): + """ + Represents a response to a request to list prompt templates. + + Args: + count: The number of prompt templates. + + resources: The list of PromptGetResponse objects. + """ + count: int + """The number of prompt templates.""" + resources: List[PromptTemplateGetResponse] + """The list of PromptGetResponse objects.""" + + +class PromptTemplateDeleteResponse(BaseModel): + """ + Represents a response to a request to delete a prompt template. + + Args: + message: The message of the response. + """ + message: str + """The message of the response.""" + + +class PromptTemplateSubstitutionRequest(BaseModel): + """ + Represents a request to substitute a prompt template. + + Args: + input_params: User provided values to replace the placeholders of the prompt template. + """ + model_config = ConfigDict(populate_by_name=True) + """Pydantic configuration to allow population by field name.""" + input_params: Optional[Dict[Any, Any]] = Field(default_factory=dict, alias='inputParams') + """User provided values to replace the placeholders of the prompt template.""" + + +class PromptTemplateSubstitutionResponse(BaseModel): + """ + Represents a response to a request to substitute a prompt template. + + Args: + parsed_prompt: The parsed prompt. + + resource: List of TemplateGetResponse objects. + """ + parsed_prompt: List[PromptTemplate] + """The parsed prompt.""" + resource: Optional[PromptTemplateGetResponse] = None + """List of TemplateGetResponse objects.""" diff --git a/packages/gen/gen_ai_hub/proxy/__init__.py b/packages/gen/gen_ai_hub/proxy/__init__.py new file mode 100644 index 0000000..4dbf79f --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/__init__.py @@ -0,0 +1,4 @@ +from .core.proxy_clients import set_proxy_version, get_proxy_version, get_proxy_client +from .gen_ai_hub_proxy import GenAIHubProxyClient + +__all__ = ('GenAIHubProxyClient', 'set_proxy_version', 'get_proxy_version', 'get_proxy_client') diff --git a/packages/gen/gen_ai_hub/proxy/core/__init__.py b/packages/gen/gen_ai_hub/proxy/core/__init__.py new file mode 100644 index 0000000..7add649 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/core/__init__.py @@ -0,0 +1,3 @@ +from .proxy_clients import get_proxy_client, get_proxy_version, proxy_version_context, set_proxy_version + +__all__ = ('set_proxy_version', 'get_proxy_version', 'proxy_version_context', 'get_proxy_client') diff --git a/packages/gen/gen_ai_hub/proxy/core/base.py b/packages/gen/gen_ai_hub/proxy/core/base.py new file mode 100644 index 0000000..192a0fd --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/core/base.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple, Type + +from pydantic import BaseModel, ConfigDict +from pydantic._internal._model_construction import ModelMetaclass + + +class BaseDeployment(BaseModel, ABC): + """Abstract base class for all deployment types. + + :param BaseModel: the base model class from Pydantic. + :type BaseModel: pydantic.BaseModel + :param ABC: the abstract base class module. + :type ABC: abc.ABC + :return: the abstract base class for deployments. + :rtype: BaseDeployment + """ + model_config = ConfigDict( + protected_namespaces=() + ) + + @abstractmethod + def additional_request_body_kwargs(self) -> Dict[str, Any]: + ... + + @property + @abstractmethod + def prediction_url(self) -> Tuple[str]: + ... + + @classmethod + @abstractmethod + def get_model_identification_kwargs(cls) -> Tuple[str]: + ... + + @classmethod + def get_main_model_identification_kwargs(cls) -> str: + return cls.get_model_identification_kwargs()[0] + + +class InstanceCacheMeta(type): + """Metaclass that caches instances based on their initialization arguments. + + :param type: the metaclass type. + :type type: type + :return: the metaclass that caches instances. + :rtype: InstanceCacheMeta + """ + _instances = {} + + def clear_cache(cls): + """Clear the instance cache.""" + cls._instances = {} + + def __call__(cls, *args, **kwargs): + key = (cls, args, tuple(sorted(kwargs.items()))) + + if key not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[key] = instance + + return cls._instances[key] + + +class CombinedMeta(InstanceCacheMeta, ModelMetaclass): + pass + + +class BaseProxyClient(ABC, BaseModel, metaclass=CombinedMeta): + """Abstract base class for all proxy clients.""" + model_config = ConfigDict( + protected_namespaces=() + ) + + @classmethod + def refresh_instance_cache(cls): + """Refresh the cache of instances.""" + InstanceCacheMeta.clear_cache(cls) + + @property + @abstractmethod + def request_header(self) -> Dict[str, Any]: + ... + + @property + @abstractmethod + def deployments(self) -> Dict[str, Any]: + ... + + @property + @abstractmethod + def deployment_class(self) -> Type[BaseDeployment]: + ... + + @abstractmethod + def select_deployment(self, **kwargs) -> BaseDeployment: + ... diff --git a/packages/gen/gen_ai_hub/proxy/core/proxy_clients.py b/packages/gen/gen_ai_hub/proxy/core/proxy_clients.py new file mode 100644 index 0000000..846ea46 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/core/proxy_clients.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import inspect +import os +import threading +import uuid +from contextlib import contextmanager +from typing import Optional + +from .base import BaseProxyClient + +THREAD_PROXY_VERSION_OVERWRITE = 'proxy_version_overwrite' +PROXY_VERSION_ENV_VARIABLE = 'LLM_PROXY_VERSION' + +no_value = object() + + +class ProxyClients: + """Catalog for proxy client classes.""" + + def __init__(self) -> None: + """Initializes a ProxyClients instance.""" + + self.clients: dict[str, type[BaseProxyClient]] = {} + self._thread_local = threading.local() + self.proxy_version = os.environ.get(PROXY_VERSION_ENV_VARIABLE, None) + self.thread_variable_name = THREAD_PROXY_VERSION_OVERWRITE + str(uuid.uuid4()) + + def register(self, name: str): + """Decorator to register a proxy client class. + + :param name: The name to register the proxy client class under. + :type name: str + :raises ValueError: If the provided class is not a subclass of BaseProxyClient. + :return: A wrapper function. + :rtype: Callable + """ + def wrapper(proxy_cls: type[BaseProxyClient]) -> type[BaseProxyClient]: + """Register the proxy client class. + + :param proxy_cls: The proxy client class to register. + :type proxy_cls: type[BaseProxyClient] + :raises ValueError: If the provided class is not a subclass of BaseProxyClient. + :return: The registered proxy client class. + :rtype: type[BaseProxyClient] + """ + if not issubclass(proxy_cls, BaseProxyClient): + raise ValueError('You can only register ProxyClient subclasses') + self.clients[name] = proxy_cls + return proxy_cls + + return wrapper + + def get_proxy_cls(self, proxy_version: str | None = None) -> type[BaseProxyClient]: + """Get the proxy client class for the given version. + + :param proxy_version: The proxy version. + :type proxy_version: str | None, optional + :return: The proxy client class. + :rtype: type[BaseProxyClient] + """ + proxy_version = proxy_version or get_proxy_version() + return self.clients[proxy_version] + + def get_proxy_cls_name(self, proxy_client_cls: type[BaseProxyClient] | BaseProxyClient) -> str: + """Get the name of the proxy client class. + + :param proxy_client_cls: The proxy client class or instance. + :type proxy_client_cls: type[BaseProxyClient] | BaseProxyClient + :raises ValueError: If the provided class is not a subclass of BaseProxyClient. + :raises KeyError: If the class is not registered. + :return: The name of the proxy client class. + :rtype: str + """ + if not inspect.isclass(proxy_client_cls): + proxy_client_cls = type(proxy_client_cls) + if not issubclass(proxy_client_cls, BaseProxyClient): + raise ValueError("'proxy_client_cls' has to be a ProxyClient class or an instance of an ProxyClient class.") + + for name, cls in self.clients.items(): + if cls is proxy_client_cls: + return name + raise KeyError(f'{proxy_client_cls} is not a registered client.') + + +@contextmanager +def proxy_version_context(proxy_version: str, catalog: Optional[ProxyClients] = None) -> None: + """Context manager to set a thread-local proxy version. + + :param proxy_version: The proxy version to set. + :type proxy_version: str + :param catalog: The catalog for which the proxy version is to be set. If none is provided, + the proxy version is set for the default proxy_clients catalog. + :type catalog: Optional[ProxyClients], optional + :raises ValueError: If proxy_version is not a string. + """ + catalog = catalog or proxy_clients + if not isinstance(proxy_version, str): + raise ValueError('proxy_version has to be a string') + + old_value = getattr(catalog._thread_local, catalog.thread_variable_name, no_value) + setattr(catalog._thread_local, catalog.thread_variable_name, proxy_version.lower()) + + try: + yield + finally: + # Restore the original value when exiting the context + if old_value is no_value: + delattr(catalog._thread_local, catalog.thread_variable_name) + else: + setattr(catalog._thread_local, catalog.thread_variable_name, old_value) + + +def set_proxy_version(proxy_version: str, catalog: Optional[ProxyClients] = None) -> None: + """Set the global proxy version. + + :param proxy_version: The proxy version to set. + :type proxy_version: str + :param catalog: The catalog for which the proxy version is to be set. If none is provided, + the proxy version is set for the default proxy_clients catalog. + :type catalog: Optional[ProxyClients], optional + :raises ValueError: If proxy_version is not a string. + """ + catalog = catalog or proxy_clients + if not isinstance(proxy_version, str): + raise ValueError('proxy_version has to be a string') + + catalog.proxy_version = proxy_version.lower() + + +def get_proxy_version(catalog: Optional[ProxyClients] = None) -> str: + """Get the current proxy version. The version is selected in the following order: + + - thread-local overwrite (set with proxy_version_context) + + - global overwrite (set with set_proxy_version) + + - environment variable (LLM_PROXY_VERSION) + + - first registered proxy version + + :param catalog: The catalog from which to get the proxy version. If none is provided, + :type catalog: Optional[ProxyClients], optional + :raises ValueError: If no proxy version is set. + :return: The current proxy version. + :rtype: str + """ + + catalog = catalog or proxy_clients + thread_overwrite = getattr(catalog._thread_local, catalog.thread_variable_name, None) + env_version = os.environ.get(PROXY_VERSION_ENV_VARIABLE, None) + fallback_version = [*catalog.clients.keys()][0] if catalog.clients else None + proxy_version = thread_overwrite or catalog.proxy_version or env_version or fallback_version + if not proxy_version: + raise ValueError('No proxy version set. Please use set_proxy_version or proxy_version_context.') + return proxy_version + + +def get_proxy_client(proxy_version: str | None = None, + catalog: Optional[ProxyClients] = None, + **kwargs) -> BaseProxyClient: + """Get a proxy client for the given proxy version. + + :param proxy_version: The version of the proxy client to retrieve. If not provided, the function will + attempt to retrieve the version using the `get_proxy_version` function. + :type proxy_version: str | None, optional + :param catalog: The catalog from which to retrieve the proxy client. If not provided, the function will + default to the `proxy_clients` catalog + :type catalog: Optional[ProxyClients], optional + :param kwargs: Arbitrary keyword arguments that will be passed to the constructor of the proxy client class. + :type kwargs: dict + :return: An instance of the proxy client. + :rtype: BaseProxyClient + """ + + catalog = catalog or proxy_clients + proxy_version = proxy_version or get_proxy_version() + return catalog.get_proxy_cls(proxy_version)(**kwargs) + + +proxy_clients = ProxyClients() diff --git a/packages/gen/gen_ai_hub/proxy/core/utils.py b/packages/gen/gen_ai_hub/proxy/core/utils.py new file mode 100644 index 0000000..b8403c9 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/core/utils.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import time +import warnings +from functools import lru_cache, wraps +from typing import Dict, Literal, Optional, List, Any, Tuple + + +def _get_cache_refresh_time(cache_refresh_time: float, recache: bool, timeout: Optional[int] = None) -> float: + current_time = time.time() + if recache or (timeout is not None and (current_time - cache_refresh_time) > timeout): + cache_refresh_time = current_time + return cache_refresh_time + + +def _get_cache_key_and_args(cache_refresh_time: float, args: List[Any], first_arg_self: bool) -> Tuple[ + Optional[Any], List[Any], float]: + if first_arg_self and args: + id_ = id(args[0]) + cache_key = (id_, cache_refresh_time) + obj, args = args[0], args[1:] + else: + cache_key = (cache_refresh_time,) + obj = None + return cache_key, args, obj + + +def lru_cache_extended(timeout: Optional[int] = None, + maxsize: Optional[int] = None, + typed: bool = False, + first_arg_self: bool = False): + """Decorator to add LRU caching with optional timeout to methods. + Handles 'self' as a weak reference for instance methods if required. + + :param timeout: time in seconds after which the cache will be refreshed. If None, never expires. + :type timeout: Optional[int], optional + :param maxsize: maximum size of the cache. + :type maxsize: Optional[int], optional + :param typed: if True, arguments of different types will be cached separately. + :type typed: bool, optional + :param first_arg_self: if True, treats the first argument as 'self' and uses its id for caching. + :type first_arg_self: bool, optional + :return: Decorated method with cache and optional timeout. + :rtype: Callable + """ + + def decorator(func): + cache_refresh_time = time.time() + + objs = {} + + @wraps(func) + @lru_cache(maxsize=maxsize, typed=typed) + def cached_method(cache_key, *args, **kwargs): + if first_arg_self: + id_, _ = cache_key + args = (objs.pop(id_),) + (args or tuple([])) + + return func(*args, **kwargs) + + @wraps(func) + def wrapped_func(*args, _recache=False, **kwargs): + nonlocal cache_refresh_time + cache_refresh_time = _get_cache_refresh_time(cache_refresh_time, _recache, timeout) + cache_key, args, obj = _get_cache_key_and_args(cache_refresh_time, args, first_arg_self) + if obj: + objs[id(obj)] = obj + try: + ret = cached_method(cache_key, *args, **kwargs) + finally: + if first_arg_self: + objs.pop(id(obj), None) + return ret + + wrapped_func.cache_info = cached_method.cache_info + wrapped_func.cache_clear = cached_method.cache_clear + + return wrapped_func + + return decorator + + +try: + # Don't duplicate the definition if openai offers it + from openai._types import NOT_GIVEN, NotGiven +except ImportError: + class NotGiven: + + def __bool__(self) -> Literal[False]: + return False + + NOT_GIVEN = NotGiven() + + +def if_set(value, alternative=NOT_GIVEN): + """Check if a value is set (not NotGiven and not None), otherwise return an alternative. + + :param value: the value to check. + :type value: any + :param alternative: the alternative value to return if the original value is not set. + :type alternative: any + :return: The original value if set, otherwise the alternative. + :rtype: any + """ + return value if not isinstance(value, NotGiven) and value is not None else alternative + +def if_str_set(value: str, alternative: str = ""): + """Check if a string value is set (not empty), otherwise return an alternative. + + :param value: the string value to check. + :type value: str + :param alternative: the alternative string to return if the original value is empty. + :type alternative: str, optional + :return: The original string if not empty, otherwise the alternative. + :rtype: str + """ + return value if value != "" else alternative + +def kwargs_if_set(**kwargs): + """Filter keyword arguments to include only those that are set (not NotGiven and not None). + + :return: A dictionary of keyword arguments that are set. + :rtype: Dict[str, any] + """ + filtered_kwargs = {} + for name in [*kwargs.keys()]: + if if_set(kwargs[name]): + filtered_kwargs[name] = kwargs[name] + return filtered_kwargs + +def warn_once(msg, category=None): + """Issue a warning only once for a given message. + + :param msg: the warning message. + :type msg: str + :param category: the warning category. + :type category: Optional[Warning], optional + """ + if not getattr(warn_once, 'log', None): + warn_once.log = set() + if msg not in warn_once.log: + warnings.warn(msg, category, stacklevel=2) + warn_once.log.add(msg) diff --git a/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/__init__.py b/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/__init__.py new file mode 100644 index 0000000..62a7893 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/__init__.py @@ -0,0 +1,3 @@ +from .client import GenAIHubProxyClient, temporary_headers_addition + +__all__ = ('GenAIHubProxyClient', 'temporary_headers_addition') diff --git a/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/client.py b/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/client.py new file mode 100644 index 0000000..184148b --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/gen_ai_hub_proxy/client.py @@ -0,0 +1,514 @@ +from __future__ import annotations + +import os + +import re +from concurrent.futures import ThreadPoolExecutor, as_completed +from contextlib import contextmanager +from contextvars import ContextVar +from copy import deepcopy +from datetime import datetime +from enum import Enum +from fnmatch import fnmatch +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union + +from ai_api_client_sdk.models.status import Status +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from pydantic import BaseModel, PrivateAttr, model_validator, Field, ConfigDict, ValidationError + +from gen_ai_hub.proxy.core.base import BaseDeployment, BaseProxyClient +from gen_ai_hub.proxy.core.proxy_clients import proxy_clients +from gen_ai_hub.proxy.core.utils import warn_once + +_temporary_headers_addition = ContextVar('temporary_headers_addition', default={}) + + +class GenAIHubRestClient: + """REST client with automatic header injection. + + This client wraps the AI Core rest_client and ensures that all requests include: + - Instance-level headers (set via proxy_client.set_headers_addition) + - Request-level headers (set via temporary_headers_addition context manager) + + :param proxy_client: The GenAIHubProxyClient instance to get the rest_client and headers from. + """ + + def __init__(self, proxy_client: 'GenAIHubProxyClient'): + """Initialize the GenAIHubRestClient. + + :param proxy_client: The GenAIHubProxyClient instance to get the rest_client and headers from. + """ + self._rest_client = proxy_client.ai_core_client.rest_client + self._proxy_client = proxy_client + + def _inject_headers(self, kwargs: dict) -> dict: + """Inject additional headers into the request kwargs. + + Only injects headers if there are additional headers to add. + Base headers (AI-Client-Type, AI-Resource-Group) and Authorization + are handled by the underlying RestClient. + + :param kwargs: The keyword arguments passed to the request method. + :return: Updated kwargs with injected headers. + """ + additional = self._proxy_client.get_additional_headers() + if not additional: + return kwargs # No additional headers to inject + + headers = dict(additional) + if kwargs.get('headers'): + headers.update(kwargs['headers']) # Explicit headers take precedence + kwargs['headers'] = headers + return kwargs + + def get(self, path: str, **kwargs): + """Send a GET request with injected headers. + + :param path: The API path. + :param kwargs: Additional arguments to pass to the underlying rest_client. + :return: The response from the rest_client. + """ + return self._rest_client.get(path=path, **self._inject_headers(kwargs)) + + def post(self, path: str, **kwargs): + """Send a POST request with injected headers. + + :param path: The API path. + :param kwargs: Additional arguments to pass to the underlying rest_client. + :return: The response from the rest_client. + """ + return self._rest_client.post(path=path, **self._inject_headers(kwargs)) + + def delete(self, path: str, **kwargs): + """Send a DELETE request with injected headers. + + :param path: The API path. + :param kwargs: Additional arguments to pass to the underlying rest_client. + :return: The response from the rest_client. + """ + return self._rest_client.delete(path=path, **self._inject_headers(kwargs)) + + def patch(self, path: str, **kwargs): + """Send a PATCH request with injected headers. + + :param path: The API path. + :param kwargs: Additional arguments to pass to the underlying rest_client. + :return: The response from the rest_client. + """ + return self._rest_client.patch(path=path, **self._inject_headers(kwargs)) + + +@contextmanager +def temporary_headers_addition(headers: Dict[str, str]): + """Context manager to temporarily add headers to requests made by the GenAIHubProxyClient. + + :param headers: Headers to add temporarily. + :type headers: Dict[str, str] + """ + previous_temporary_headers = _temporary_headers_addition.set(headers) + + try: + yield + finally: + _temporary_headers_addition.reset(previous_temporary_headers) + + +class Deployment(BaseDeployment): + """Deployment class represents a deployment of a foundational model in the GenAI Hub.""" + url: str + config_id: str + config_name: str + deployment_id: str + model_name: str + model_version: Optional[str] = None + created_at: datetime + additonal_parameters: Dict[str, str] = Field(default_factory=dict) + custom_prediction_suffix: Optional[str] = None + + def __getattr__(self, name): + """Get attribute from additional parameters if not found in the Deployment instance. + + :param name: Attribute name to get. + :type name: str + :raises err: If attribute not found in both Deployment instance and additional parameters. + :return: Attribute value. + :rtype: Any + """ + try: + super().__getattr__(name) + except AttributeError as err: + self.additonal_parameters: Dict[str, str] + value = self.additonal_parameters.get(name, None) + if value: + return value + raise err + + # abstractmethod implementations + def additional_request_body_kwargs(self) -> Dict[str, Any]: + return {} + + @classmethod + def get_model_identification_kwargs(cls) -> Tuple[str]: + """Get model identification keywords. + + :return: Tuple of model identification keywords. + :rtype: Tuple[str] + """ + return ('model_name', 'model_version', 'config_id', 'config_name', 'deployment_id') + + @property + def prediction_url(self): + if self.model_name.startswith("cohere"): + return self.url.rstrip('/?') + '/chat' if self.url else '' + return None + + +class FoundationalModelScenario(BaseModel): + """Represents a foundational model scenario in the GenAI Hub.""" + model_config = ConfigDict( + protected_namespaces=() + ) + + scenario_id: str + config_names: Optional[Union[List[str], str]] = None + model_name_parameter: str = 'model_name' + prediction_url_suffix: Optional[str] = None + + @model_validator(mode='before') + @classmethod + def adjust(cls, data: Any) -> Any: + """Adjust input data before model initialization. + + :param data: Input data to adjust. + :type data: Any + :return: Adjusted data. + :rtype: Any + """ + if isinstance(data, dict): + config_names = data.get('config_names', None) + if isinstance(config_names, str): + data['config_names'] = [config_names] + elif config_names is None: + data['config_names'] = ['*'] + prediction_url_suffix = data.get('prediction_url_suffix', None) + if prediction_url_suffix is not None: + data['prediction_url_suffix'] = '/' + prediction_url_suffix.lstrip('/') + return data + + +def _deployment_matches(deployment: BaseDeployment, **search_key_value: Dict[str, str]): + match = False + for key, value in search_key_value.items(): + if value is None: + continue + deployment_value = getattr(deployment, key, None) + if deployment_value is None: + continue + elif deployment_value != value: + match = False + break + elif deployment_value == value and not match: + match = True + return match + + +class InvalidDeploymentBehavior(str, Enum): + warn = 'warn' + raise_error = 'raise_error' + ignore = 'ignore' + + +@proxy_clients.register('gen-ai-hub') +class GenAIHubProxyClient(BaseProxyClient, extra='allow'): + """GenAIHubProxyClient is a proxy client for interacting with the GenAI Hub.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + + base_url: Optional[str] = None + auth_url: Optional[str] = None + client_id: Optional[str] = None + client_secret: Optional[str] = None + resource_group: Optional[str] = None + ai_core_client: Optional[AICoreV2Client] = None + + AI_CLIENT_TYPE_VAL: ClassVar[str] = 'GenAI Hub SDK (Python)' + + # class attributes + foundational_model_scenarios: ClassVar[List[FoundationalModelScenario]] = [ + FoundationalModelScenario(scenario_id='foundation-models', config_names='*', model_name_parameter='model_name'), + ] + default_values: ClassVar[Dict[str, Any]] = {} + on_invalid_deployments: ClassVar[InvalidDeploymentBehavior] = InvalidDeploymentBehavior.warn + + # private_attributes + _headers_addition: Dict[str, str] = PrivateAttr(default={}) + _deployments: List[Deployment] = PrivateAttr(default_factory=list) + + @model_validator(mode='before') + @classmethod + def init_client(cls, data: Any) -> Any: + """Initialize the client with the provided data. + + :param data: Input data for client initialization. + :type data: Any + :return: Initialized data. + :rtype: Any + """ + if isinstance(data, dict): + if data.get('ai_core_client', None) is not None: + return data + kwargs = {} + for name in ['base_url', 'auth_url', 'client_id', 'client_secret', 'resource_group']: + value = data.get(name, cls.default_values.get(name, None)) + if value is not None: + kwargs[name] = value + # Set AI-Client-Type header specific to generated AI Hub SDK. + # Is overwritten by environment variable AI_CLIENT_TYPE if set. + kwargs['client_type'] = cls.AI_CLIENT_TYPE_VAL + data['ai_core_client'] = AICoreV2Client.from_env(**kwargs) + + + return data + + # abstract method implementations + @property + def request_header(self) -> Dict[str, Any]: + return self.get_request_header() + + @property + def deployments(self) -> List[Deployment]: + return self.get_deployments() + + @property + def deployment_class(self) -> Type[Deployment]: + return Deployment + + def _get_matched_deployments(self, **search_key_value): + return [deployment for deployment in self.deployments if _deployment_matches(deployment, **search_key_value)] + + def select_deployment(self, raise_on_multiple: bool = False, **search_key_value): + if not search_key_value: + raise ValueError('No key-value pairs provided for model discovery') + + if "model_version" in search_key_value and "model_name" not in search_key_value: + raise ValueError('If a model version is specified for search, the model name must also be provided.') + + matched_deployments = self._get_matched_deployments(**search_key_value) + if len(matched_deployments) == 0: + self.update_deployments() + matched_deployments = self._get_matched_deployments(**search_key_value) + + num_matches = len(matched_deployments) + if num_matches == 1: + return matched_deployments[0] + elif num_matches > 1: + if raise_on_multiple: + raise ValueError( + "Multiple deployments match the query. Use 'raise_on_multiple=False' to return the first matching " + "deployment." + ) + return matched_deployments[0] + else: + raise ValueError('No deployment found with: ' + ', '.join( + [f'deployment.{k} == {v}' for k, v in search_key_value.items()] + )) + + # public methods + def get_additional_headers(self) -> Dict[str, str]: + """Get only the additional headers (instance-level and request-level). + + :return: Additional headers. + :rtype: Dict[str, str] + """ + headers = dict(self._headers_addition) + headers.update(_temporary_headers_addition.get()) + return headers + + def set_headers_addition(self, headers: Dict[str, str]): + """Set additional headers for requests made by the client. + + :param headers: Headers to add. + :type headers: Dict[str, str] + """ + self._headers_addition = headers + + def get_request_header(self): + """Get the request headers for requests made by the client. + + :return: Request headers. + :rtype: Dict[str, str] + """ + headers = deepcopy(self.ai_core_client.rest_client.headers) + headers.update(self.get_additional_headers()) + if os.environ.get('SKIP_AUTHORIZATION', '').lower() != 'true': + headers.update({'Authorization': self.get_ai_core_token()}) + + return headers + + def get_deployments(self): + """Get the list of deployments. + + :return: List of deployments. + :rtype: List[Deployment] + """ + if len(self._deployments) == 0: + self.update_deployments() + return self._deployments + + def _create_deployment(self, deployment, scenario_info): + try: + details = deployment.details["resources"]["backend_details"]["model"] + additional_parameters = { + "model_name": details["name"], + "model_version": details["version"] + } + except KeyError: + additional_parameters = config_parameters(scenario_info.model_name_parameter, self.ai_core_client, deployment) + return Deployment(url=deployment.deployment_url, + deployment_id=deployment.id, + created_at=deployment.created_at, + config_id=deployment.configuration_id, + config_name=deployment.configuration_name, + custom_prediction_suffix=scenario_info.prediction_url_suffix, + **additional_parameters) + + def _handle_deployment_error(self, deployment, scenario_info, err): + if self.on_invalid_deployments in [InvalidDeploymentBehavior.raise_error, + InvalidDeploymentBehavior.warn]: + msg = f'Failed to get all relevant information for deployment {deployment.id} ' + \ + f'[scenario: {scenario_info.scenario_id}; config: {deployment.configuration_name}]! ' + \ + 'If this deployment is an LLM deployment make sure to set the model_name_parameter ' + \ + 'when registring the foundation model scenario or use a more rigorous ' + \ + 'config name filter. If the deployment is in the default foundation model scenario ' + \ + 'consider using a different scenario for you deployments.' + if self.on_invalid_deployments == InvalidDeploymentBehavior.raise_error: + raise RuntimeError(msg) from err + else: + warn_once(msg) + elif self.on_invalid_deployments == InvalidDeploymentBehavior.ignore: + pass + else: + raise ValueError(f'Invalid value for on_invalid_deployments: {self.on_invalid_deployments}') + + def _get_scenario_deployments(self, scenario_info: FoundationalModelScenario): + deployments = {} + query = self.ai_core_client.deployment.query(status=Status.RUNNING, scenario_id=scenario_info.scenario_id) + + resources_to_process = [ + deployment + for deployment in query.resources + if any( + fnmatch(deployment.configuration_name, n) + for n in scenario_info.config_names + ) + ] + + with ThreadPoolExecutor() as executor: + future_to_deployment = { + executor.submit( + self._create_deployment, deployment, scenario_info + ): deployment + for deployment in resources_to_process + } + for future in as_completed(future_to_deployment): + deployment = future_to_deployment[future] + try: + deployments[deployment.id] = future.result() + except ValidationError as err: + self._handle_deployment_error(deployment, scenario_info, err) + + return deployments + + def update_deployments(self): + """Update the list of deployments from the GenAI Hub. + + :return: List of updated deployments. + :rtype: List[Deployment] + """ + deployment_set = {} # use dict to remove duplicates based on deployment id + # self.select_deployment.cache_clear() # pylint: disable=no-member + for scenario_info in self.foundational_model_scenarios: + deployment_set.update(self._get_scenario_deployments(scenario_info)) + self._deployments = [*deployment_set.values()] + self._deployments = sorted(self._deployments, key=lambda d: d.created_at, reverse=True) + return self._deployments + + @classmethod + def add_foundation_model_scenario(cls, + scenario_id, + config_names: Optional[List[str]] = None, + prediction_url_suffix: Optional[str] = None, + model_name_parameter: str = 'model_name'): + """Add a foundational model scenario to the client. + + :param scenario_id: the scenario ID. + :type scenario_id: str + :param config_names: list of configuration names, defaults to None + :type config_names: Optional[List[str]], optional + :param prediction_url_suffix: prediction URL suffix, defaults to None + :type prediction_url_suffix: Optional[str], optional + :param model_name_parameter: model name parameter, defaults to 'model_name' + :type model_name_parameter: str, optional + """ + cls.foundational_model_scenarios.append( + FoundationalModelScenario(scenario_id=scenario_id, + config_names=config_names, + model_name_parameter=model_name_parameter, + prediction_url_suffix=prediction_url_suffix)) + for client in cls._instances.values(): + client._deployments = [] + + def get_ai_core_token(self): + """Get the AI core token for authentication. + + :return: AI core token. + :rtype: str + """ + return self.ai_core_client.rest_client.get_token() + + @classmethod + def set_default_values(cls, **kwargs): + """Set default values for the client.""" + cls.default_values.update(kwargs) + + @classmethod + def for_profile(cls, profile: str = None): + """Create a GenAIHubProxyClient instance for the given profile. + + :param profile: Profile name, defaults to None + :type profile: str, optional + :return: GenAIHubProxyClient instance. + :rtype: GenAIHubProxyClient + """ + return cls(ai_core_client=AICoreV2Client.from_env(profile_name=profile)) + + +def camel_to_snake(name): + """Convert camelCase or PascalCase string to snake_case. + + :param name: Input string in camelCase or PascalCase. + :type name: str + :return: String converted to snake_case. + :rtype: str + """ + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +def config_parameters(model_name_parameter, ai_core_client, deployment): + """Get configuration parameters for a deployment. + + :param model_name_parameter: the model name parameter. + :type model_name_parameter: str + :param ai_core_client: the AI core client. + :type ai_core_client: AICoreV2Client + :param deployment: the deployment. + :type deployment: Deployment + :return: Dictionary with model name and additional parameters. + :rtype: Dict[str, Any] + """ + model_parameters = {} + config = ai_core_client.configuration.get(deployment.configuration_id) + model_parameters['executable_id'] = config.executable_id + for param in config.parameter_bindings: + model_parameters[camel_to_snake(param.key)] = param.value + return {'model_name': model_parameters.pop(model_name_parameter, None), 'additonal_parameters': model_parameters} diff --git a/packages/gen/gen_ai_hub/proxy/langchain/__init__.py b/packages/gen/gen_ai_hub/proxy/langchain/__init__.py new file mode 100644 index 0000000..5c2927f --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/__init__.py @@ -0,0 +1,51 @@ +from .init_models import init_embedding_model, init_llm +from .openai import OpenAI, OpenAIEmbeddings, OpenAIClient, AsyncOpenAIClient, ChatOpenAI +from .amazon import BedrockEmbeddings, AICoreBedrockBaseModel, ChatBedrock, ChatBedrockConverse +from .google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings + +# Make sure that the user has a recent version of langchain installed +_langchain_is_below_0_1_0 = None +try: + import langchain +except ImportError: + pass +else: + from packaging import version + _langchain_is_below_0_1_0 = version.parse(langchain.__version__) < version.parse('0.1.0') + if _langchain_is_below_0_1_0: + raise ImportError('langchain<0.1.0 is no longer supported. Please upgrade to version 0.1.0 or higher.') + + +__all__ = [ + 'init_llm', + 'init_embedding_model', + 'OpenAIClient', + 'OpenAIEmbeddings', + 'AsyncOpenAIClient', + 'ChatOpenAI', + 'BedrockEmbeddings', + 'AICoreBedrockBaseModel', + 'ChatBedrock', + 'ChatBedrockConverse', + 'ChatGoogleGenerativeAI', + 'GoogleGenerativeAIEmbeddings', + 'OpenAI' +] + +try: + from .openai import ChatOpenAI, OpenAI, OpenAIEmbeddings + __all__.extend(['ChatOpenAI', 'OpenAI', 'OpenAIEmbeddings']) +except ImportError: + pass + +try: + from .amazon import ChatBedrock + __all__.append('ChatBedrock') +except ImportError: + pass + +try: + from .google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings + __all__.extend(['ChatGoogleGenerativeAI', 'GoogleGenerativeAIEmbeddings']) +except ImportError: + pass diff --git a/packages/gen/gen_ai_hub/proxy/langchain/amazon.py b/packages/gen/gen_ai_hub/proxy/langchain/amazon.py new file mode 100644 index 0000000..9f3d92c --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/amazon.py @@ -0,0 +1,322 @@ +import logging +from typing import Dict, Optional, List, Any + +from botocore.config import Config +from langchain_aws import ChatBedrock as ChatBedrock_, ChatBedrockConverse as ChatBedrockConverse_ +from langchain_community.embeddings import BedrockEmbeddings as BedrockEmbeddings_ +from pydantic import BaseModel, ConfigDict, model_validator + +from gen_ai_hub.proxy.core.base import BaseProxyClient +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import Deployment +from gen_ai_hub.proxy.native.amazon.clients import Session + + +def _parse_minor_version(raw: str) -> tuple: + """Parses a Claude minor version like ``4`` or ``4.6`` into a comparable tuple.""" + if "." in raw: + major, minor = raw.split(".", 1) + return int(major), int(minor) + return int(raw), 0 + + +class AICoreBedrockBaseModel(BaseModel): + """AICoreBedrockBaseModel provides all adjustments + to boto3 based LangChain classes to enable communication + with SAP AI Core.""" + + model_config = ConfigDict(extra='allow') + + def __init__( + self, + *args, + model_id: str = "", + deployment_id: str = "", + model_name: str = "", + config_id: str = "", + config_name: str = "", + proxy_client: Optional[BaseProxyClient] = None, + **kwargs, + ): + """Initializes the AICoreBedrockBaseModel with AICore specific parameters. + Extends the constructor of the base class with aicore specific parameters + + :param model_id: the model identifier, defaults to "" + :type model_id: str, optional + :param deployment_id: the deployment identifier, defaults to "" + :type deployment_id: str, optional + :param model_name: the model name, defaults to "" + :type model_name: str, optional + :param config_id: the configuration identifier, defaults to "" + :type config_id: str, optional + :param config_name: the configuration name, defaults to "" + :type config_name: str, optional + :param proxy_client: the proxy client to use, defaults to None + :type proxy_client: Optional[BaseProxyClient], optional + """ + client_params = { + "deployment_id": deployment_id, + "model_name": model_name, + "config_id": config_id, + "config_name": config_name, + "proxy_client": proxy_client, + } + kwargs["client_params"] = client_params + super().__init__(*args, model_id=model_id, **kwargs) + + @staticmethod + def get_corresponding_model_id(full_model_name, model_version='latest'): + """Gets the corresponding model ID for a given model name. + :param full_model_name: the model name + :type full_model_name: str + :param model_version: the model version + :type model_version: str + :return: the corresponding model ID + :rtype: str + """ + provider, model_name = full_model_name.split("--", maxsplit=1) + if model_name.startswith('claude'): + claude, base_model_version, base_model_name = model_name.split('-') + base_model_version_major, base_model_version_minor = _parse_minor_version(base_model_version) + if base_model_version_major >= 4: + # Reorder: claude-{name}-{major}-{minor} + model_name = '-'.join([claude, base_model_name, str(base_model_version_major), str(base_model_version_minor)]) + return '.'.join([provider, model_name]) + return f"{provider}.{model_name}-{model_version}" + + # pylint: disable=no-self-argument + @model_validator(mode='before') + def validate_environment(cls, values: Dict) -> Dict: + """Validates and sets up the environment for the model. + + :param values: the input values + :type values: Dict + :return: the validated values + :rtype: Dict + """ + client_params = values.get("client_params") + if not client_params and "model_kwargs" in values and isinstance(values["model_kwargs"], dict): + client_params = values["model_kwargs"].get("client_params") + + if client_params and not values.get("client"): + if "config" in values and values["config"] is not None: + client_params["config"] = values["config"] + values["client"] = Session().client(**client_params) + + if values.get('model_id') in (None, ''): + deployment = values["client"].aicore_deployment + values["model_id"] = cls.get_corresponding_model_id( + deployment.model_name, + deployment.model_version + ) + + # Remove client_params from model_kwargs to prevent it from being passed to AWS API + if "model_kwargs" in values and isinstance(values["model_kwargs"], dict): + values["model_kwargs"].pop("client_params", None) + + # Remove client_params from top level to prevent it from being passed to AWS API + values.pop("client_params", None) + + return values + + +class ChatBedrock(AICoreBedrockBaseModel, ChatBedrock_): + """Drop-in replacement for LangChain ChatBedrock.""" + + model_config = ConfigDict(extra='allow') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class ChatBedrockConverse(AICoreBedrockBaseModel, ChatBedrockConverse_): + """Drop-in replacement for LangChain ChatBedrockConverse.""" + + model_config = ConfigDict(extra='allow') + + def __init__(self, *args, **kwargs): + self.extract_model_kwargs_parameters(kwargs) + + super().__init__(*args, **kwargs) + + def extract_model_kwargs_parameters(self, kwargs): + """Extracts specific parameters from model_kwargs and moves them to the top level of kwargs. + + :param kwargs: the input keyword arguments + :type kwargs: Dict + """ + # Extract parameters from model_kwargs to avoid circular reference issues + model_kwargs = kwargs.get('model_kwargs', {}) + if isinstance(model_kwargs, dict): + # Extract common parameters that should be passed directly + for param_name in ['temperature', 'max_tokens', 'top_p', 'stop_sequences']: + if param_name in model_kwargs and param_name not in kwargs: + kwargs[param_name] = model_kwargs.pop(param_name) + + # Clean up model_kwargs if it's now empty + if not model_kwargs: + kwargs.pop('model_kwargs', None) + else: + kwargs['model_kwargs'] = model_kwargs + + +class BedrockEmbeddings(AICoreBedrockBaseModel, BedrockEmbeddings_): + """Drop-in replacement for LangChain BedrockEmbeddings.""" + + model_config = ConfigDict(extra='allow') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +def _build_bedrock_model_kwargs( + deployment: Deployment, + temperature: float, + max_tokens: int, + top_k: Optional[int], + top_p: float, + stop_sequences: Optional[List[str]], + config: Optional[Config] +) -> Dict[str, Any]: + """Builds the model_kwargs dictionary for Bedrock models.""" + if top_k: + logging.warning( + "Top-k is disabled for Amazon Bedrock models. Ignoring top-k value." + ) + + model_kwargs = { + "temperature": temperature, + } + + if config: + model_kwargs["config"] = config + + if deployment.model_name.startswith("anthropic"): + model_kwargs["max_tokens"] = max_tokens + model_kwargs["top_p"] = top_p + else: # Assuming Amazon bedrock models otherwise + model_kwargs["maxTokenCount"] = max_tokens + model_kwargs["topP"] = top_p + if stop_sequences: + model_kwargs["stopSequences"] = stop_sequences + + return model_kwargs + +def init_chat_model( + proxy_client: BaseProxyClient, + deployment: Deployment, + temperature: float = 0.0, + max_tokens: int = 256, + top_k: Optional[int] = None, + top_p: float = 1.0, + stop_sequences: List[str] = None, + model_id: Optional[str] = '', + config: Optional[Config] = None +): + """Initializes a chat model using the legacy Bedrock Invoke API (`ChatBedrock`). + + :param proxy_client: the proxy client to use + :type proxy_client: BaseProxyClient + :param deployment: the deployment information + :type deployment: Deployment + :param temperature: the temperature for the model, defaults to 0.0 + :type temperature: float, optional + :param max_tokens: the maximum number of tokens to generate, defaults to 256 + :type max_tokens: int, optional + :param top_k: the top-k sampling parameter, defaults to None + :type top_k: Optional[int], optional + :param top_p: the top-p sampling parameter, defaults to 1.0 + :type top_p: float, optional + :param stop_sequences: the stop sequences for the model, defaults to None + :type stop_sequences: List[str], optional + :param model_id: the model identifier, defaults to '' + :type model_id: Optional[str], optional + :param config: the botocore configuration, defaults to None + :type config: Optional[Config], optional + :return: the initialized chat model + :rtype: ChatBedrock + """ + + model_kwargs = _build_bedrock_model_kwargs( + deployment=deployment, + temperature=temperature, + max_tokens=max_tokens, + top_k=top_k, + top_p=top_p, + stop_sequences=stop_sequences, + config=config + ) + + return ChatBedrock( + model_name=deployment.model_name, + model_id=model_id, + deployment_id=deployment.deployment_id, + proxy_client=proxy_client, + model_kwargs=model_kwargs + ) + +def init_chat_converse_model( + proxy_client: BaseProxyClient, + deployment: Deployment, + temperature: float = 0.0, + max_tokens: int = 256, + top_k: Optional[int] = None, + top_p: float = 1.0, + stop_sequences: List[str] = None, + model_id: Optional[str] = '', + config: Optional[Config] = None +): + """Initializes a chat model using the newer Bedrock Converse API (`ChatBedrockConverse`). + The Converse API offers several advantages over the older Invoke API: + + - Unified interface for different models and modalities. + + - Native support for tool use (function calling). + + - Standardized request/response structure. + + :param proxy_client: the proxy client to use + :type proxy_client: BaseProxyClient + :param deployment: the deployment information + :type deployment: Deployment + :param temperature: the temperature for the model, defaults to 0.0 + :type temperature: float, optional + :param max_tokens: the maximum number of tokens to generate, defaults to 256 + :type max_tokens: int, optional + :param top_k: the top-k sampling parameter, defaults to None + :type top_k: Optional[int], optional + :param top_p: the top-p sampling parameter, defaults to 1.0 + :type top_p: float, optional + :param stop_sequences: the stop sequences for the model, defaults to None + :type stop_sequences: List[str], optional + :param model_id: the model identifier, defaults to '' + :type model_id: Optional[str], optional + :param config: the botocore configuration, defaults to None + :type config: Optional[Config], optional + :return: the initialized chat model + :rtype: ChatBedrockConverse + """ + return ChatBedrockConverse( + model_name=deployment.model_name, + model_id=model_id, + deployment_id=deployment.deployment_id, + proxy_client=proxy_client, + temperature=temperature, + max_tokens=max_tokens, + top_p=top_p, + stop_sequences=stop_sequences, + config = config + ) + +def init_embedding_model(proxy_client: BaseProxyClient, deployment: Deployment, model_id: Optional[str] = ''): + """Initializes an embedding model using BedrockEmbeddings. + + :param proxy_client: the proxy client to use + :type proxy_client: BaseProxyClient + :param deployment: the deployment information + :type deployment: Deployment + :param model_id: the model identifier, defaults to '' + :type model_id: Optional[str], optional + :return: the initialized embedding model + :rtype: BedrockEmbeddings + """ + return BedrockEmbeddings(deployment_id=deployment.deployment_id, proxy_client=proxy_client, model_id=model_id) diff --git a/packages/gen/gen_ai_hub/proxy/langchain/base.py b/packages/gen/gen_ai_hub/proxy/langchain/base.py new file mode 100644 index 0000000..9f3ac9d --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/base.py @@ -0,0 +1,33 @@ +from typing import Any, Optional + +from pydantic import BaseModel + +from gen_ai_hub.proxy.core import get_proxy_client + +_VALUES = [('proxy_model_name', 'model_name'), ('deployment_id', 'deployment_id'), ('config_id', 'config_id'), + ('config_name', 'config_name')] + + +class BaseAuth(BaseModel): + """Base class for authentication models. + + :param BaseModel: The base model class to inherit from. + :type BaseModel: pydantic.BaseModel + :return: An instance of the BaseAuth class. + :rtype: BaseAuth + """ + proxy_client: Optional[Any] = None #: :meta private: + deployment_id: Optional[str] = None + config_name: Optional[str] = None + config_id: Optional[str] = None + proxy_model_name: Optional[str] = None + + @classmethod + def _get_proxy_client(cls, values): + return values.get('proxy_client', None) or get_proxy_client() + + @staticmethod + def _set_deployment_parameters(values, deployment): + for key_values, key_deployment in _VALUES: + if hasattr(deployment, key_deployment): + values[key_values] = getattr(deployment, key_deployment) diff --git a/packages/gen/gen_ai_hub/proxy/langchain/google_genai.py b/packages/gen/gen_ai_hub/proxy/langchain/google_genai.py new file mode 100644 index 0000000..6226870 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/google_genai.py @@ -0,0 +1,167 @@ +"""Drop-in replacements for langchain_google_genai models with SAP AI Core integration.""" + +from typing import Optional + +from langchain_google_genai import ChatGoogleGenerativeAI as ChatGoogleGenerativeAI_ +from langchain_google_genai import GoogleGenerativeAIEmbeddings as GoogleGenerativeAIEmbeddings_ +from pydantic import model_validator, ConfigDict + +from gen_ai_hub.proxy.core.base import BaseProxyClient +from gen_ai_hub.proxy.core.utils import if_str_set +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import Deployment +from gen_ai_hub.proxy.native.google_genai.clients import Client as GenAIHubNativeClient + + +class _BaseGoogleGenerativeAI: + """Base class for Google Generative AI models with common functionality.""" + + model_config = ConfigDict(extra='allow') + + def __init__( + self, + model: str = "", + proxy_model_name: str = "", + model_id: str = "", + deployment_id: str = "", + config_id: str = "", + config_name: str = "", + proxy_client: Optional[BaseProxyClient] = None, + **kwargs, + ): + """ + Parameters: + model (str): The model name to use. + proxy_model_name (str): The proxy model name to use. + model_id (str): The model ID to use. + deployment_id (str): The deployment ID to use. + config_id (str): The configuration ID to use. + config_name (str): The configuration name to use. + proxy_client (Optional[BaseProxyClient]): The proxy client to use. + **kwargs: Additional keyword arguments. + """ + resolved_model_name, updated_kwargs = self._setup_client_and_kwargs( + model=model, + proxy_model_name=proxy_model_name, + model_id=model_id, + deployment_id=deployment_id, + config_id=config_id, + config_name=config_name, + proxy_client=proxy_client, + kwargs=kwargs, + ) + + # Call the appropriate parent class constructor + self._init_parent(model=resolved_model_name, **updated_kwargs) + + def _init_parent(self, **kwargs): + raise NotImplementedError("Subclasses must implement _init_parent") + + def _setup_client_and_kwargs( + self, + model: str, + proxy_model_name: str, + model_id: str, + deployment_id: str, + config_id: str, + config_name: str, + proxy_client: Optional[BaseProxyClient], + kwargs: dict, + ) -> tuple[str, dict]: + if model_id != "": + raise ValueError( + "Parameter not supported. Please use a variation of deployment_id, " + "model_name, config_id and config_name to identify a deployment." + ) + + resolved_model_name = if_str_set( + model, if_str_set(proxy_model_name, kwargs.get("model_name", "")) + ) + + native_client = GenAIHubNativeClient( + proxy_client=proxy_client, + deployment_id=deployment_id, + config_id=config_id, + config_name=config_name + ) + + kwargs["client"] = native_client + kwargs["google_api_key"] = "dummy_key" + + return resolved_model_name, kwargs + + @model_validator(mode="before") + @classmethod + def validate_environment(cls, values: dict) -> dict: + """Override to prevent LangChain from looking for local GOOGLE_API_KEY env vars.""" + if not values.get("google_api_key"): + values["google_api_key"] = "dummy_key" + return values + + @model_validator(mode="after") + def _determine_backend(self): + """Override to skip backend determination since we use our own client.""" + object.__setattr__(self, "_use_vertexai", True) + return self + + @model_validator(mode="after") + def _initialize_client(self): + """Override to prevent the parent class from creating its own client.""" + return self + + +class ChatGoogleGenerativeAI(_BaseGoogleGenerativeAI, ChatGoogleGenerativeAI_): + """Drop-in replacement for langchain_google_genai.ChatGoogleGenerativeAI.""" + + def _init_parent(self, **kwargs): + ChatGoogleGenerativeAI_.__init__(self, **kwargs) + + +class GoogleGenerativeAIEmbeddings(_BaseGoogleGenerativeAI, GoogleGenerativeAIEmbeddings_): + """Drop-in replacement for langchain_google_genai.GoogleGenerativeAIEmbeddings.""" + + def _init_parent(self, **kwargs): + GoogleGenerativeAIEmbeddings_.__init__(self, **kwargs) + +def init_chat_model( + proxy_client: BaseProxyClient, + deployment: Deployment, + temperature: float = 0.0, + max_tokens: int = 256, + top_k: Optional[int] = None, + top_p: float = 1.0, +): + """Initialize a ChatGoogleGenerativeAI model with the given parameters. + + :param proxy_client: proxy client to use for the model + :type proxy_client: BaseProxyClient + :param deployment: deployment information for the model + :type deployment: Deployment + :param temperature: sampling temperature, defaults to 0.0 + :type temperature: float, optional + :param max_tokens: maximum number of tokens to generate, defaults to 256 + :type max_tokens: int, optional + :param top_k: k for top-k sampling, defaults to None + :type top_k: Optional[int], optional + :param top_p: p for nucleus sampling, defaults to 1.0 + :type top_p: float, optional + :return: initialized ChatGoogleGenerativeAI model + :rtype: ChatGoogleGenerativeAI + """ + return ChatGoogleGenerativeAI( + model=deployment.model_name, + deployment_id=deployment.deployment_id, + proxy_client=proxy_client, + temperature=temperature, + max_output_tokens=max_tokens, + top_k=top_k, + top_p=top_p, + ) + +def init_embedding_model(proxy_client: BaseProxyClient, deployment: Deployment): + return GoogleGenerativeAIEmbeddings( + model=deployment.model_name, + deployment_id=deployment.deployment_id, + proxy_client=proxy_client, + config_id=getattr(deployment, 'config_id', ''), + config_name=getattr(deployment, 'config_name', ''), + ) diff --git a/packages/gen/gen_ai_hub/proxy/langchain/init_models.py b/packages/gen/gen_ai_hub/proxy/langchain/init_models.py new file mode 100644 index 0000000..0f3794a --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/init_models.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from enum import Enum, auto +from typing import Any, Callable, Dict, List, Optional, Union + +from langchain_core.embeddings import Embeddings # pylint: disable=import-error, no-name-in-module +from langchain_core.language_models import BaseLanguageModel + +from gen_ai_hub.proxy.core import get_proxy_client +from gen_ai_hub.proxy.core.base import BaseDeployment, BaseProxyClient +from gen_ai_hub.proxy.langchain import amazon, google_genai, openai + + +def default_f_select_deployment(proxy_client: BaseProxyClient, + **model_identification_kwargs: Dict[str, str]) -> BaseDeployment: + """Default function to select a deployment based on model identification kwargs. + + :param proxy_client: The proxy client to use for selecting the deployment + :type proxy_client: BaseProxyClient + :return: The selected deployment + :rtype: BaseDeployment + """ + return proxy_client.select_deployment(**model_identification_kwargs) + + +def handle_model_args_kwargs(proxy_client, args: List[Any], kwargs: Dict[str, Any]): + """Handles model identification arguments and keyword arguments. + + :param proxy_client: the proxy client to use for model identification + :type proxy_client: _type_ + :param args: list of positional arguments + :type args: List[Any] + :param kwargs: dictionary of keyword arguments + :type kwargs: Dict[str, Any] + :raises ValueError: if no model identification argument is provided + :return: A tuple containing the model name, model identification kwargs, and remaining kwargs + :rtype: Tuple[str, Dict[str, str], Dict[str, Any]] + """ + main_kwarg = proxy_client.deployment_class.get_main_model_identification_kwargs() + kwarg_names = proxy_client.deployment_class.get_model_identification_kwargs() + if args: + model_name = args[0] + kwargs[main_kwarg] = model_name + elif main_kwarg in kwargs: + model_name = kwargs[main_kwarg] + else: + raise ValueError('No model identification argument provided') + model_identification_kwargs = {n: kwargs[n] for n in kwarg_names if n in kwargs} + return model_name, model_identification_kwargs, kwargs + + +class ModelType(Enum): + LLM = auto() + EMBEDDINGS = auto() + + +def _init_custom_model(proxy_client: BaseProxyClient, init_func: Callable, args: List[Any], kwargs: Dict[str, Any], + model_kwargs: Dict[str, Any]): + proxy_client = proxy_client or get_proxy_client() + model_name, model_identification_kwargs, kwargs = handle_model_args_kwargs(proxy_client=proxy_client, args=args, + kwargs=kwargs) + + try: + deployment = default_f_select_deployment(proxy_client, **model_identification_kwargs) + except ValueError: + proxy_client.update_deployments() + deployment = default_f_select_deployment(proxy_client, **model_identification_kwargs) + return init_func(proxy_client=proxy_client, deployment=deployment, **model_kwargs) + + +def _get_init_func(model_name: str, model_type: ModelType): + if any(model_name.startswith(prefix) for prefix in ['amazon', 'anthropic']): + if model_type == ModelType.EMBEDDINGS: + return amazon.init_embedding_model + else: + return amazon.init_chat_model + elif any(model_name.startswith(prefix) for prefix in ['google', 'gemini']): + if model_type == ModelType.EMBEDDINGS: + return google_genai.init_embedding_model + else: + return google_genai.init_chat_model + else: + if model_type == ModelType.EMBEDDINGS: + return openai.init_embedding_model + else: + return openai.init_chat_model + + + +def _init_model(proxy_client: Optional[BaseProxyClient], + model_type: ModelType, + args: List[Any], + kwargs: Dict[str, Any], + init_func: Optional[Callable] = None, + model_kwargs: Optional[Dict[str, Any]] = None): + model_kwargs = model_kwargs or {} + if init_func: + return _init_custom_model(proxy_client=proxy_client, init_func=init_func, args=args, kwargs=kwargs, + model_kwargs=model_kwargs) + model_name, model_identification_kwargs, kwargs = handle_model_args_kwargs(proxy_client=proxy_client, args=args, + kwargs=kwargs) + init_func = _get_init_func(model_name, model_type) + deployment = default_f_select_deployment(proxy_client, **model_identification_kwargs) + return init_func(proxy_client=proxy_client, deployment=deployment, **model_kwargs) + + +def init_llm(*args, + proxy_client: Optional[BaseProxyClient] = None, + temperature: float = 0.0, + max_tokens: int = 256, + top_k: Optional[int] = None, + top_p: float = 1., + init_func: Optional[Callable] = None, + model_id: Optional[str] = '', + **kwargs) -> BaseLanguageModel: + """ + Initializes a language model using the specified parameters. + + :param proxy_client: The proxy client to use for the model (optional) + :type proxy_client: ProxyClient + :param temperature: The temperature parameter for model generation (default: 0.0) + :type temperature: float + :param max_tokens: The maximum number of tokens to generate (default: 256) + :type max_tokens: int + :param top_k: The top-k parameter for model generation (optional) + :type top_k: int + :param top_p: The top-p parameter for model generation (default: 1.0) + :type top_p: float + :param init_func: Function to call for initializing the model, optional + :type init_func: Callable + :param model_id: id of the Amazon Bedrock model, needed in case a custom Amazon Bedrock model is being + initiated (optional) + :type model_id: str + :return: The initialized language model + :rtype: BaseLanguageModel + """ + model_kwargs = { + 'temperature': temperature, + 'max_tokens': max_tokens, + 'top_k': top_k, + 'top_p': top_p, + } + if model_id: + model_kwargs['model_id'] = model_id + if 'config' in kwargs: + model_kwargs['config'] = kwargs["config"] + return _init_model(args=args, + proxy_client=proxy_client, + model_type=ModelType.LLM, + model_kwargs=model_kwargs, + init_func=init_func, + kwargs=kwargs) + + +def init_embedding_model(*args, + proxy_client: Optional[BaseProxyClient] = None, + init_func: Optional[Callable] = None, + model_id: Optional[str] = '', + **kwargs) -> Embeddings: + """ + Initializes an embedding model using the specified parameters. + + :param proxy_client: The proxy client to use for the model (optional) + :type proxy_client: BaseProxyClient + :param init_func: Function to call for initializing the model, optional + :type init_func: Callable + :param model_id: id of the Amazon Bedrock model, needed in case a custom Amazon Bedrock model is being + initiated (optional) + :type model_id: str + :return: The initialized embedding model + :rtype: Embeddings + """ + model_kwargs = {} + if model_id: + model_kwargs['model_id'] = model_id + return _init_model(args=args, + proxy_client=proxy_client, + model_type=ModelType.EMBEDDINGS, + model_kwargs=model_kwargs, + init_func=init_func, + kwargs=kwargs) diff --git a/packages/gen/gen_ai_hub/proxy/langchain/openai.py b/packages/gen/gen_ai_hub/proxy/langchain/openai.py new file mode 100644 index 0000000..3690e47 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/langchain/openai.py @@ -0,0 +1,370 @@ +"""LangChain wrappers for OpenAI models via Generative AI Hub.""" +import re +from typing import Any, Dict, Optional + +from langchain_core.messages import AIMessage +from langchain_core.outputs import ChatGeneration, ChatResult +from langchain_openai import ChatOpenAI as ChatOpenAI_ # pylint: disable=import-error, no-name-in-module +from langchain_openai import OpenAI as OpenAI_ # pylint: disable=import-error, no-name-in-module +from langchain_openai import OpenAIEmbeddings as OpenAIEmbeddings_ # pylint: disable=import-error, no-name-in-module +from pydantic import Field, model_validator, ConfigDict + +from gen_ai_hub.proxy.core.base import BaseDeployment, BaseProxyClient +from gen_ai_hub.proxy.native.openai import AsyncOpenAI as AsyncOpenAIClient +from gen_ai_hub.proxy.native.openai import OpenAI as OpenAIClient +from gen_ai_hub.proxy.native.openai.clients import DEFAULT_API_VERSION +from .base import BaseAuth + + +def get_client_params(values): + """ + Get the client parameters. + :param values: The client values + :return: client values + proxy_client + """ + return { + 'api_key': values.get('openai_api_key', 'EMPTY') or 'EMPTY', + 'organization': values.get('openai_organization', 'EMPTY') or 'EMPTY', + 'proxy_client': values.get('proxy_client', None), + 'timeout': values.get('request_timeout', None), + 'max_retries': values.get('max_retries', 2), + 'default_headers': values.get('default_headers', {}) or {}, + 'default_query': values.get('default_query', {}) or {}, + 'http_client': values.get('http_client', None), + 'api_version': values.get('openai_api_version', DEFAULT_API_VERSION) or DEFAULT_API_VERSION + } + + +class ProxyOpenAI(BaseAuth): + """Base class for OpenAI models using a proxy. + + :param BaseAuth: Base authentication class + :type BaseAuth: class + :return: The ProxyOpenAI class + :rtype: class + """ + model_config = ConfigDict(extra='allow') + + @classmethod + def validate_clients(cls, values: Dict) -> Dict: + """Validate and initialize OpenAI clients. + + :param values: The input values + :type values: Dict + :return: The validated values + :rtype: Dict + """ + values['proxy_client'] = cls._get_proxy_client(values) + client_params = get_client_params(values) + if not values.get('client'): + values['client'] = OpenAIClient(**client_params) + if not values.get('async_client'): + values['async_client'] = AsyncOpenAIClient(**client_params) + deployment = values['proxy_client'].select_deployment( + deployment_id=values.get('deployment_id', None), + config_id=values.get('config_id', None), + config_name=values.get('config_name', None), + model_name=values.get('proxy_model_name', None), + ) + BaseAuth._set_deployment_parameters(values, deployment) + return values + + +class ChatOpenAI(ProxyOpenAI, ChatOpenAI_): + """ChatOpenAI model using a proxy. + + :param ProxyOpenAI: Base class for OpenAI models using a proxy + :type ProxyOpenAI: class + :param ChatOpenAI_: ChatOpenAI class from langchain_openai + :type ChatOpenAI_: class + """ + model_name: Optional[str] = None + openai_api_version: Optional[str] = Field(default=None, alias='api_version') + + model_config = ConfigDict(extra='allow') + + def __new__(cls, **data: Any): # type: ignore + """ + Initialize the OpenAI object. + :param data: Additional data to initialize the object + :type data: Any + :return: The initialized OpenAI object + :rtype: OpenAIBase + """ + data['model_name'] = data.get('model_name', '') or '' + return ChatOpenAI_.__new__(cls) + + # pylint: disable=no-self-argument + def __init__(self, *args, **kwargs): + """Initialize the ChatOpenAI object.""" + n = kwargs.pop('n', 1) + super().__init__(*args, openai_api_key='???', n=n, **kwargs) + + @model_validator(mode='before') + def validate_environment(cls, values: Dict) -> Dict: + """Validates the environment. + + :param values: The input values + :type values: Dict + :raises ValueError: n must be at least 1. + :return: The validated values + :rtype: Dict + """ + values = cls.validate_clients(values) + if values['n'] < 1: + raise ValueError('n must be at least 1.') + if values['n'] > 1 and values['streaming']: + raise ValueError('n must be 1 when streaming.') + if isinstance(values['client'], OpenAIClient): + values['root_client'] = values['client'] # Store full client for beta API access + values['client'] = values['client'].chat.completions + if isinstance(values['async_client'], AsyncOpenAIClient): + values['root_async_client'] = values['async_client'] # Store full client for beta API access + values['async_client'] = values['async_client'].chat.completions + values['model_name'] = values.get('model_name', None) or values.get('model', None) or values.get( + 'proxy_model_name', None) + return values + + @property + def _default_params(self) -> Dict[str, Any]: + """Returns the default parameters for the OpenAI object. + + :return: The default parameters + :rtype: Dict[str, Any] + """ + return { + **super()._default_params, 'deployment_id': self.deployment_id, + 'config_id': self.config_id, + 'config_name': self.config_name, + 'model_name': self.proxy_model_name, + 'model': '' + } + + def _create_chat_result(self, response: Any, generation_info: Optional[Dict[str, Any]] = None) -> Any: + """Override to handle Cohere's response format which uses model_extra instead of choices. + + :param response: The response from the API. + :type response: Any + :param generation_info: Additional generation information. + :type generation_info: Optional[Dict[str, Any]] + :return: The chat result. + :rtype: Any + """ + + # Check if this is a Cohere model response + if (hasattr(response, 'model_extra') and + response.model_extra and + 'message' in response.model_extra and + self.proxy_model_name and + re.search(r"^cohere--", self.proxy_model_name)): + + # Extract content from Cohere's response format + cohere_message = response.model_extra.get('message', {}) + content_parts = cohere_message.get('content', []) + + content_text = "" + for part in content_parts: + if isinstance(part, dict) and part.get('type') == 'text': + content_text += part.get('text', '') + + message = AIMessage(content=content_text) + generation = ChatGeneration(message=message) + token_usage = response.usage.model_extra if response.usage and response.usage.model_extra else {} + + return ChatResult(generations=[generation], llm_output=token_usage) + + # For non-Cohere models, use the default implementation + return super()._create_chat_result(response) + + +class OpenAI(ProxyOpenAI, OpenAI_): + """OpenAI model using a proxy.""" + model_name: Optional[str] = None + openai_api_version: Optional[str] = Field(default=None, alias='api_version') + + model_config = ConfigDict(extra='allow') + + def __init__(self, *args, **kwargs): + """Initialize the OpenAI object.""" + n = kwargs.pop('n', 1) + super().__init__(*args, openai_api_key='???', n=n, **kwargs) + + def __new__(cls, **data: Any): # type: ignore + """Initialize the OpenAI object.""" + data['model_name'] = data.get('model_name', '') or '' + return OpenAI_.__new__(cls) + + # pylint: disable=no-self-argument + @model_validator(mode='before') + def validate_environment(cls, values: Dict) -> Dict: + """Validates the environment. + + :param values: The input values + :type values: Dict + :return: The validated values + :rtype: Dict + """ + values = cls.validate_clients(values) + if values['n'] < 1: + raise ValueError('n must be at least 1.') + if values['n'] > 1 and values['streaming']: + raise ValueError('n must be 1 when streaming.') + if isinstance(values['client'], OpenAIClient): + values['root_client'] = values['client'] + values['client'] = values['client'].completions + if isinstance(values['async_client'], AsyncOpenAIClient): + values['root_async_client'] = values['async_client'] + values['async_client'] = values['async_client'].completions + values['model_name'] = values.get('model_name', None) or values.get('model', None) or values.get( + 'proxy_model_name', None) + return values + + @property + def _default_params(self) -> Dict[str, Any]: + params = super()._default_params + return { + **params, 'deployment_id': self.deployment_id, + 'config_id': self.config_id, + 'config_name': self.config_name, + 'model_name': self.proxy_model_name, + 'model': '' + } + + +class OpenAIEmbeddings(ProxyOpenAI, OpenAIEmbeddings_): + """OpenAI Embeddings model using a proxy.""" + model: Optional[str] = None + tiktoken_model_name: Optional[str] = 'text-embedding-ada-002' + chunk_size: int = 16 + openai_api_version: Optional[str] = Field(default=None, alias='api_version') + input_type: Optional[str] = Field( + default=None, + description="Required for NVIDIA models: 'query' for search queries, 'passage' for documents. " + ) + + model_config = ConfigDict(extra='allow') + + def __init__(self, *args, **kwargs): + """Initialize the OpenAIEmbeddings object.""" + super().__init__(*args, openai_api_key='???', **kwargs) + + # pylint: disable=no-self-argument + @model_validator(mode='before') + def validate_environment(cls, values: Dict) -> Dict: + """Validates the environment. + + :param values: The input values + :type values: Dict + :return: The validated values + :rtype: Dict + """ + values = cls.validate_clients(values) + if isinstance(values['client'], OpenAIClient): + values['client']: OpenAIClient = values['client'].embeddings + if isinstance(values['async_client'], AsyncOpenAIClient): + values['async_client']: AsyncOpenAIClient = values['async_client'].embeddings + values['model'] = values.get('model', None) or values.get('model_name', None) or values.get( + 'proxy_model_name', None) + return values + + def _get_len_safe_embeddings(self, texts, *, engine, **kwargs): + """Override embedding creation to handle NVIDIA-specific requirements. + + For NVIDIA models: + - Requires explicit input_type parameter: 'query' for search queries, 'passage' for documents + - Bypasses Langchain's tokenization to send raw text strings directly to the API + (NVIDIA models require string input, not tokenized arrays) + - Filters out Langchain-specific parameters not supported by OpenAI API + - Returns standard embedding format: List[List[float]] + + For non-NVIDIA models, delegates to parent class implementation. + """ + if self.model and 'nvidia' in self.model.lower(): + if not self.input_type: + raise ValueError(f"input_type parameter is required for NVIDIA models (model: {self.model}).") + + if self.input_type not in ('query', 'passage'): + raise ValueError( + f"input_type must be either 'query' or 'passage', got '{self.input_type}'" + ) + + kwargs['extra_body'] = {'input_type': self.input_type} + + api_kwargs = {key: val for key, val in kwargs.items() + if key not in ('chunk_size', 'engine')} + + # Send raw text strings directly (no tokenization) + response = self.client.create( + input=texts, + model_name=self.model, + deployment_id=self.deployment_id, + **api_kwargs + ) + return [list(map(float, e.embedding)) for e in response.data] + + return super()._get_len_safe_embeddings(texts, engine=engine, **kwargs) + + @property + def _invocation_params(self) -> Dict: + params = super()._invocation_params + return { + **params, + 'deployment_id': self.deployment_id, + 'config_id': self.config_id, + 'config_name': self.config_name, + 'model': self.model, + } + + +def init_chat_model( + proxy_client: BaseProxyClient, + deployment: BaseDeployment, + temperature: float = 0.0, + max_tokens: int = 256, + top_k: Optional[int] = None, + top_p: float = 1.0, +): + """Initialize the ChatOpenAI model. + + :param proxy_client: the proxy client + :type proxy_client: BaseProxyClient + :param deployment: the deployment + :type deployment: BaseDeployment + :param temperature: the temperature, defaults to 0.0 + :type temperature: float, optional + :param max_tokens: the maximum tokens, defaults to 256 + :type max_tokens: int, optional + :param top_k: the top k, defaults to None + :type top_k: Optional[int], optional + :param top_p: the top p, defaults to 1.0 + :type top_p: float, optional + :return: the ChatOpenAI model + :rtype: ChatOpenAI + """ + return ChatOpenAI( + deployment_id=deployment.deployment_id, + proxy_client=proxy_client, + temperature=temperature, + max_tokens=max_tokens, + top_p=top_p, + ) + + +def init_embedding_model(proxy_client: BaseProxyClient, deployment: BaseDeployment): + """Initialize the OpenAIEmbeddings model. + + :param proxy_client: the proxy client + :type proxy_client: BaseProxyClient + :param deployment: the deployment + :type deployment: BaseDeployment + :return: the OpenAIEmbeddings model + :rtype: OpenAIEmbeddings + """ + kwarg = { + 'deployment_id': deployment.deployment_id, + 'proxy_client': proxy_client, + 'chunk_size': 16, + } + if 'nvidia' in deployment.model_name.lower(): + return OpenAIEmbeddings(input_type='query', **kwarg) + return OpenAIEmbeddings(**kwarg) diff --git a/packages/gen/gen_ai_hub/proxy/native/__init__.py b/packages/gen/gen_ai_hub/proxy/native/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/gen_ai_hub/proxy/native/amazon/__init__.py b/packages/gen/gen_ai_hub/proxy/native/amazon/__init__.py new file mode 100644 index 0000000..e973b15 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/amazon/__init__.py @@ -0,0 +1,5 @@ +from .clients import (set_deployment, get_current_deployment, prepare_request_dict, tolerate_missing_model_id, + ClientWrapper, Session, AsyncSession, AsyncClientWrapper) + +__all__ = ["set_deployment", "get_current_deployment","prepare_request_dict", "tolerate_missing_model_id", + "ClientWrapper", "Session", "AsyncSession", "AsyncClientWrapper"] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/proxy/native/amazon/clients.py b/packages/gen/gen_ai_hub/proxy/native/amazon/clients.py new file mode 100644 index 0000000..6577f0c --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/amazon/clients.py @@ -0,0 +1,419 @@ +""" +This module provides client wrappers for synchronous and asynchronous interactions +with the Amazon Bedrock Runtime service. +""" +import asyncio +import contextvars +import warnings +from contextlib import contextmanager +from typing import Optional + +import aiobotocore.session +from aiobotocore.client import AioBaseClient +from aiobotocore.config import AioConfig +from boto3 import Session as Session_ +from botocore import UNSIGNED +from botocore.client import BaseClient +from botocore.config import Config + +from gen_ai_hub.proxy.core import get_proxy_client +from gen_ai_hub.proxy.core.base import BaseProxyClient +from gen_ai_hub.proxy.core.utils import if_str_set, kwargs_if_set + +# required for testing framework in llm-commons +_current_deployment = contextvars.ContextVar("current_deployment") + + +@contextmanager +def set_deployment(value): + """Sets the current deployment in a context. + + :param value: The deployment to set. + :type value: Any + """ + token = _current_deployment.set(value) + try: + yield + finally: + _current_deployment.reset(token) + + +def get_current_deployment(): + """Gets the current deployment from the context. + + :return: The current deployment. + :rtype: Any + """ + return _current_deployment.get(None) + + +def prepare_request_dict(request_dict, aicore_deployment, aicore_proxy_client): + """Prepares the request dictionary for the AI Core proxy. + + :param request_dict: the request dictionary to prepare + :type request_dict: dict + :param aicore_deployment: the AI Core deployment + :type aicore_deployment: object + :param aicore_proxy_client: the AI Core proxy client + :type aicore_proxy_client: object + :return: the prepared request dictionary + :rtype: dict + """ + url_extension = request_dict["url_path"].rsplit("/", 1)[-1] + request_dict["url_path"] = url_extension + request_dict["url"] = f"{aicore_deployment.url.rstrip('/')}/{url_extension.lstrip('/')}" + del request_dict["headers"]["User-Agent"] + request_dict["headers"] = { + 'accept': 'application/vnd.amazon.eventstream', + **request_dict["headers"], + **aicore_proxy_client.request_header, + } + return request_dict + + +def tolerate_missing_model_id(kwargs): + """Tolerates missing modelId in kwargs.""" + if "modelId" not in kwargs: + kwargs["modelId"] = "notapplicable" + return kwargs + + +class ClientWrapper(BaseClient): + """Wraps and extends the boto3 BedrockRuntime class. + boto3 is implemented in a way that a bedrock runtime + class is created on the fly. Regular inheritance is + therefor not possible. Instead, this wrapper inherits + from the boto3 BaseClient class and is initialised + with an instance of the bedrock runtime object. All + attributes of the bedrock runtime object are copied + over to the ClientWrapper object. Methods that need + to be adjusted are regularly overwritten in case they + are defined in the base class BaseClient (orginating + from botocore). In case methods need to be adjusted + that are dynamically added, they are also overwritten + in regular fashion. The linter will not be able to verify + the super methods existence though.""" + + def __init__(self, client, aicore_deployment, aicore_proxy_client): + """Initializes the ClientWrapper. + + :param client: the boto3 bedrock runtime client + :type client: BaseClient + :param aicore_deployment: the AI Core deployment + :type aicore_deployment: object + :param aicore_proxy_client: the AI Core proxy client + :type aicore_proxy_client: object + """ + # copy over all object attributes to the wrapper object + self.__class__ = type( + client.__class__.__name__, + (self.__class__, client.__class__), + {}, + ) + self.__dict__ = client.__dict__ + + self.aicore_deployment = aicore_deployment + self.aicore_proxy_client = aicore_proxy_client # called proxy_client in other sdk integrations + + def _convert_to_request_dict(self, *args, **kwargs): + request_dict = super()._convert_to_request_dict(*args, **kwargs) + return prepare_request_dict(request_dict, self.aicore_deployment, self.aicore_proxy_client) + + def invoke_model(self, *args, **kwargs): + """Tolerates missing parameters and calls original + invoke_model method. + """ + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return super().invoke_model(*args, **kwargs) + + # pylint: disable=invalid-name + def invoke_model_with_response_stream(self, *args, **kwargs): + """ + Tolerates missing parameters and calls original invoke_model_with_response_stream method. + + If the user provides a timeout parameter, it is removed and ignored. + Issues a deprecation warning. + """ + timeout = kwargs.pop("timeout", None) + if timeout is not None: + warnings.warn("The timeout parameter is ignored. " + "Timeouts should be defined via Session().client configuration.", DeprecationWarning, + stacklevel=2) + + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return super().invoke_model_with_response_stream(*args, **kwargs) + + def converse(self, *args, **kwargs): + """Tolerates missing parameters and calls original + converse method. + """ + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return super().converse(*args, **kwargs) + + def converse_stream(self, *args, **kwargs): + """Tolerates missing parameters and calls original + converse_stream method. + """ + tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return super().converse_stream(*args, **kwargs) + + +class AsyncClientWrapper(AioBaseClient): + """Async client wrapper extending AioBaseClient of aiobotocore which provides async support for botocore.""" + + # pylint: disable=super-init-not-called + def __init__(self, client, aicore_deployment, aicore_proxy_client, context_manager): + """Initializes the AsyncClientWrapper. + + :param client: the aiobotocore bedrock runtime client + :type client: AioBaseClient + :param aicore_deployment: the AI Core deployment + :type aicore_deployment: object + :param aicore_proxy_client: the AI Core proxy client + :type aicore_proxy_client: object + :param context_manager: the client context manager for cleanup + :type context_manager: context manager + """ + # copy over all object attributes to the wrapper object + self.__class__ = type( + client.__class__.__name__, + (self.__class__, client.__class__), + {}, + ) + self.__dict__ = client.__dict__ + + self.aicore_deployment = aicore_deployment + self.aicore_proxy_client = aicore_proxy_client + self._context_manager = context_manager + + def __del__(self): + """Cleanup when object is garbage collected.""" + if self._context_manager is not None: + # Schedule cleanup in the event loop + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(self._close()) + else: + loop.run_until_complete(self._close()) + # pylint: disable=broad-except + except Exception as _ignored: + # Best effort cleanup - don't raise exceptions in __del__ + pass + + async def close(self): + """Closes the client and releases resources.""" + await self._close() + await super().close() + + async def _close(self): + """Internal cleanup helper.""" + if self._context_manager is not None: + try: + await self._context_manager.__aexit__(None, None, None) + # pylint: disable=broad-except + except Exception: + pass # Ignore exceptions during cleanup + finally: + self._context_manager = None + + async def __aenter__(self): + """Enter async context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Exit async context manager and close the client.""" + await self._close() + + async def _convert_to_request_dict(self, *args, **kwargs): + request_dict = await super()._convert_to_request_dict(*args, **kwargs) + return prepare_request_dict(request_dict, self.aicore_deployment, self.aicore_proxy_client) + + async def invoke_model(self, *args, **kwargs): + """Tolerates missing parameters and calls original + invoke_model method. + """ + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return await super().invoke_model(*args, **kwargs) + + # pylint: disable=invalid-name + async def invoke_model_with_response_stream(self, *args, **kwargs): + """ + Tolerates missing parameters and calls original invoke_model_with_response_stream method. + + If the user provides a timeout parameter, it is removed and ignored. + Issues a deprecation warning. + """ + timeout = kwargs.pop("timeout", None) + if timeout is not None: + warnings.warn("The timeout parameter is ignored. " + "Timeouts should be defined via Session().client configuration.", DeprecationWarning, + stacklevel=2) + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return await super().invoke_model_with_response_stream(*args, **kwargs) + + async def converse(self, *args, **kwargs): + """Tolerates missing parameters and calls original + converse method. + """ + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return await super().converse(*args, **kwargs) + + async def converse_stream(self, *args, **kwargs): + """Tolerates missing parameters and calls original + converse method. + """ + kwargs = tolerate_missing_model_id(kwargs) + # pylint: disable=no-member + return await super().converse_stream(*args, **kwargs) + + +class Session(Session_): + """Drop-in replacement for boto3.Session that uses + the current deployment for amazon bedrock models""" + + def client( + self, + *args, + model: str = "", + deployment_id: str = "", + model_name: str = "", + model_version: str = "", + config_id: str = "", + config_name: str = "", + proxy_client: Optional[BaseProxyClient] = None, + **kwargs, + ): + """Creates client for the bedrock runtime service. + + :param model: the model identifier, defaults to "" + :type model: str, optional + :param deployment_id: the deployment identifier, defaults to "" + :type deployment_id: str, optional + :param model_name: the model name, defaults to "" + :type model_name: str, optional + :param model_version: the model version, defaults to "" + :type model_version: str, optional + :param config_id: the config identifier, defaults to "" + :type config_id: str, optional + :param config_name: the config name, defaults to "" + :type config_name: str, optional + :param proxy_client: the proxy client, defaults to None + :type proxy_client: Optional[BaseProxyClient], optional + :raises NotImplementedError: if service_name is not bedrock-runtime + :return: the bedrock runtime client + :rtype: ClientWrapper + """ + proxy = proxy_client or get_proxy_client() + model_name = if_str_set(model_name, if_str_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy.select_deployment(**model_identification) + + config = Config(signature_version=UNSIGNED) + if "config" in kwargs: + config = config.merge(kwargs["config"]) + del kwargs["config"] + if "region_name" in kwargs: + del kwargs["region_name"] + if "service_name" in kwargs and kwargs["service_name"] != "bedrock-runtime": + raise NotImplementedError("Only bedrock-runtime service is supported.") + client = super().client( + *args, + config=config, + region_name="notapplicable", + service_name="bedrock-runtime", + **kwargs, + ) + with set_deployment(deployment): + return ClientWrapper(client, get_current_deployment(), proxy) + + +# pylint: disable=too-few-public-methods +class AsyncSession: + """Async session for Amazon Bedrock models that uses + the current deployment for amazon bedrock models. + + Uses aiobotocore directly for async operations. + """ + + def __init__(self, **_kwargs): + """Initializes the AsyncSession. + + All keyword arguments are passed to aiobotocore session configuration. + """ + self._session = aiobotocore.session.get_session() + + # pylint: disable=too-many-arguments + async def async_client( + self, + *args, + model: str = "", + deployment_id: str = "", + model_name: str = "", + model_version: str = "", + config_id: str = "", + config_name: str = "", + proxy_client: Optional[BaseProxyClient] = None, + **kwargs, + ): + """Creates async client for the bedrock runtime service. + + :param model: the model identifier, defaults to "" + :type model: str, optional + :param deployment_id: the deployment identifier, defaults to "" + :type deployment_id: str, optional + :param model_name: the model name, defaults to "" + :type model_name: str, optional + :param model_version: the model version, defaults to "" + :type model_version: str, optional + :param config_id: the config identifier, defaults to "" + :type config_id: str, optional + :param config_name: the config name, defaults to "" + :type config_name: str, optional + :param proxy_client: the proxy client, defaults to None + :type proxy_client: Optional[BaseProxyClient], optional + :raises NotImplementedError: if service_name is not bedrock-runtime + :return: the bedrock runtime async client + :rtype: AsyncClientWrapper + """ + proxy = proxy_client or get_proxy_client() + model_name = if_str_set(model_name, if_str_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy.select_deployment(**model_identification) + config = AioConfig(signature_version=UNSIGNED) + if "config" in kwargs: + config = config.merge(kwargs["config"]) + del kwargs["config"] + if "region_name" in kwargs: + del kwargs["region_name"] + if "service_name" in kwargs and kwargs["service_name"] != "bedrock-runtime": + raise NotImplementedError("Only bedrock-runtime service is supported.") + context_manager = self._session.create_client( + service_name="bedrock-runtime", + *args, + config=config, + region_name="notapplicable", + **kwargs, + ) + client = await context_manager.__aenter__() + with set_deployment(deployment): + return AsyncClientWrapper(client, get_current_deployment(), proxy, context_manager) diff --git a/packages/gen/gen_ai_hub/proxy/native/google_genai/__init__.py b/packages/gen/gen_ai_hub/proxy/native/google_genai/__init__.py new file mode 100644 index 0000000..098efe5 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/google_genai/__init__.py @@ -0,0 +1,3 @@ +from .clients import Client, AICoreDynamicTransport, AsyncAICoreDynamicTransport + +__all__ = ['Client', 'AICoreDynamicTransport', 'AsyncAICoreDynamicTransport'] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py b/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py new file mode 100644 index 0000000..4dd6f63 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/google_genai/clients.py @@ -0,0 +1,249 @@ +from typing import Optional, Union +import httpx +from google.genai import Client as GoogleClient +from google.genai import types +from google.genai.models import Models as GoogleModels +from google.oauth2.credentials import Credentials + +from gen_ai_hub.proxy.core.base import BaseProxyClient +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client +from gen_ai_hub.proxy.core.utils import kwargs_if_set + +_deployment_cache = {} + +def _resolve_deployment(transport_instance, requested_model_name: str): + """ + Finds the appropriate AI Core deployment based on the model name and any filtering kwargs in model_identification. + """ + + if requested_model_name in _deployment_cache: + return _deployment_cache[requested_model_name] + + model_identification = transport_instance.get_selector_kwargs().copy() + + model_identification['model_name'] = requested_model_name + + if requested_model_name == "gemini-embedding-001": + model_identification['model_name'] = "gemini-embedding" + + deployment = transport_instance.proxy_client.select_deployment(**model_identification) + + _deployment_cache[requested_model_name] = deployment + return deployment + + +def _rewrite_request(transport_instance, request: httpx.Request): + """Interception and rewriting of the request. Handles dynamic routing, header injection, etc.""" + + path = request.url.path + + if "/models/" not in path: + return request + + _, suffix = path.split("/models/", 1) + + # If the path ends at /models (suffix is empty) or /models? (suffix starts with ?), + # it is a discovery call, not an inference call, and cannot be routed through SAP AI Core. + if not suffix or suffix.startswith("/") or suffix.startswith("?"): + return request + + # Suffix is like "{model_name}:generateContent" + if ":" in suffix: + model_name = suffix.split(":")[0] + else: + model_name = suffix + + deployment = _resolve_deployment(transport_instance, model_name) + + deployment_url = httpx.URL(deployment.url) + + # Path construction: deployment_url + /models/ + {model_name}:generateContent + new_path = f"{deployment_url.path.rstrip('/')}/models/{suffix}" + + request.url = request.url.copy_with( + scheme=deployment_url.scheme, + host=deployment_url.host, + port=deployment_url.port, + path=new_path, + ) + + proxy_headers = transport_instance.proxy_client.request_header + + # Host must always match the resolved deployment URL host + request.headers["Host"] = deployment_url.host + + # Authorization is handled explicitly to avoid accidental overrides + auth = proxy_headers.get("Authorization") + if auth: + request.headers["Authorization"] = auth + + # Apply remaining proxy headers defensively + for header_name, header_value in proxy_headers.items(): + if header_name.lower() not in ("host", "authorization"): + request.headers[header_name] = header_value + + return request + + +class AICoreDynamicTransport(httpx.BaseTransport): + """Synchronous transport that dynamically resolves deployment URLs per request.""" + + def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): + """Transport constructor. + + :param proxy_client: The proxy client used to select deployments. + :type proxy_client: BaseProxyClient + """ + self.proxy_client = proxy_client + self._inner_transport = httpx.HTTPTransport() + self._selector_kwargs = kwargs_if_set(**deployment_selector_kwargs) + + def get_selector_kwargs(self): + """Get the deployment selector kwargs. + + :return: The deployment selector kwargs. + :rtype: dict + """ + return self._selector_kwargs + + def handle_request(self, request: httpx.Request) -> httpx.Response: + """Handles the request by rewriting it to route through the appropriate AI Core deployment. + + :param request: The original HTTPX request. + :type request: httpx.Request + :return: The HTTPX response from the AI Core deployment. + :rtype: httpx.Response + """ + modified_request = _rewrite_request(self, request) + return self._inner_transport.handle_request(modified_request) + + def close(self): + """Closes the inner transport.""" + self._inner_transport.close() + + +class AsyncAICoreDynamicTransport(httpx.AsyncBaseTransport): + """Asynchronous transport that dynamically resolves deployment URLs per request.""" + + def __init__(self, proxy_client: BaseProxyClient, **deployment_selector_kwargs): + """Transport constructor. + + :param proxy_client: The proxy client used to select deployments. + :type proxy_client: BaseProxyClient + """ + self.proxy_client = proxy_client + self._inner_transport = httpx.AsyncHTTPTransport() + self._selector_kwargs = kwargs_if_set(**deployment_selector_kwargs) + + def get_selector_kwargs(self): + """get the deployment selector kwargs. + + :return: The deployment selector kwargs. + :rtype: dict + """ + return self._selector_kwargs + + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + """Handles the request by rewriting it to route through the appropriate AI Core deployment. + + :param request: The original HTTPX request. + :type request: httpx.Request + :return: The HTTPX response from the AI Core deployment. + :rtype: httpx.Response + """ + modified_request = _rewrite_request(self, request) + return await self._inner_transport.handle_async_request(modified_request) + + async def aclose(self): + """Closes the inner transport.""" + await self._inner_transport.aclose() + +class Models(GoogleModels): + """ + Class that extends the original Google Models class for patch embed_content method. + """ + def embed_content( + self, + *, + model: str, + contents: Union[types.ContentListUnion, types.ContentListUnionDict], + config: Optional[types.EmbedContentConfigOrDict] = None, + ) -> types.EmbedContentResponse: + """Need for add model version for "gemini-embedding" model.""" + if model == "gemini-embedding": + model = "gemini-embedding-001" + return super().embed_content(model=model, contents=contents, config=config) + +class Client(GoogleClient): + """ + Unified, native-feeling client that dynamically routes requests + through SAP AI Core deployments based on the requested identifiers e.g. model name. + """ + + def __init__( + self, + # Standard Google Args (kept for compatibility, though placeholders used) + vertexai: bool = True, + project: str = "placeholder", + location: str = "placeholder", + # AI Core Filtering Args + deployment_id: str = "", + config_id: str = "", + config_name: str = "", + proxy_client: BaseProxyClient = None, + timeout: int = None, + **kwargs + ): + """Initializes the Client. + + :param vertexai: to indicate Vertex AI usage, defaults to True + :type vertexai: bool, optional + :param project: the GCP project, defaults to "placeholder" + :type project: str, optional + :param location: the GCP location, defaults to "placeholder" + :type location: str, optional + :param deployment_id: the deployment identifier, defaults to "" + :type deployment_id: str, optional + :param config_id: the configuration identifier, defaults to "" + :type config_id: str, optional + :param config_name: the configuration name, defaults to "" + :type config_name: str, optional + :param proxy_client: the proxy client to use, defaults to None + :type proxy_client: BaseProxyClient, optional + :param timeout: the request timeout, defaults to None + :type timeout: int, optional + """ + self.proxy_client = proxy_client or get_proxy_client() + + deployment_selector_kwargs = kwargs_if_set( + deployment_id=deployment_id, + config_id=config_id, + config_name=config_name, + ) + + sync_transport = AICoreDynamicTransport( + proxy_client=self.proxy_client, + **deployment_selector_kwargs + ) + async_transport = AsyncAICoreDynamicTransport( + proxy_client=self.proxy_client, + **deployment_selector_kwargs + ) + + super().__init__( + vertexai=True, + project=project, + location=location, + credentials=Credentials(token="dummy-token-placeholder"), + http_options=types.HttpOptions( + client_args={ + "transport": sync_transport + }, + async_client_args={ + "transport": async_transport + }, + timeout=timeout, + ), + **kwargs + ) + self._models = Models(self._api_client) diff --git a/packages/gen/gen_ai_hub/proxy/native/openai/__init__.py b/packages/gen/gen_ai_hub/proxy/native/openai/__init__.py new file mode 100644 index 0000000..2e8df27 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/openai/__init__.py @@ -0,0 +1,43 @@ +from typing import Any + +import openai +from packaging import version + +from gen_ai_hub.proxy.core import get_proxy_version +from .clients import AsyncOpenAI, OpenAI + +if version.parse(openai.__version__) < version.parse('1.0.0'): + raise ImportError( + f'Found openai=={openai.__version__}. Since v0.2.0 it only supports openai>=1.0.0! Update openai or run `pip install -U openai<1`' + ) +# TODO: Adjust error message + + +class GlobalClient: + """A global client to manage OpenAI clients based on proxy version.""" + + def __init__(self) -> None: + self._client = {} + + @property + def client(self): + """Get the OpenAI client based on the current proxy version. + + :return: The OpenAI client instance. + :rtype: OpenAI + """ + proxy_version = get_proxy_version() + client = self._client.get(proxy_version, None) + if not client: + self._client[proxy_version] = OpenAI() + return self._client[proxy_version] + + +_global_client = GlobalClient() + + +def __getattr__(name: str) -> Any: + if name in ('completions', 'chat', 'embeddings', "responses"): + return getattr(_global_client.client, name) + else: + return locals().get(name) diff --git a/packages/gen/gen_ai_hub/proxy/native/openai/clients.py b/packages/gen/gen_ai_hub/proxy/native/openai/clients.py new file mode 100644 index 0000000..0e0d58c --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/openai/clients.py @@ -0,0 +1,1158 @@ +from __future__ import annotations + +import contextvars +import re +from contextlib import contextmanager +from typing import Optional, Union, List, TypeVar, Iterable + +import httpx +from openai import AsyncOpenAI as AsyncOpenAI_ +from openai import OpenAI as OpenAI_ +from openai import resources +from openai._streaming import Stream, AsyncStream +from openai._types import Omit +from openai.lib._parsing._responses import TextFormatT +from openai.resources.chat import AsyncChat as AsyncChat_ +from openai.resources.chat import Chat as Chat_ +from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions_ +from openai.resources.chat.completions import Completions as ChatCompletions_ +from openai.resources.completions import AsyncCompletions as AsyncCompletions_ +from openai.resources.completions import Completions as Completions_ +from openai.resources.embeddings import AsyncEmbeddings as AsyncEmbeddings_ +from openai.resources.embeddings import Embeddings as Embeddings_ +from openai.resources.responses import Responses as Responses_ +from openai.resources.responses import AsyncResponses as AsyncResponses_ +from openai.types import Completion, Embedding +from openai.types.chat import ChatCompletion, ChatCompletionMessageParam +from openai.types.chat.parsed_chat_completion import ParsedChatCompletion +from openai.types.responses import Response, ResponseStreamEvent, ResponseInputParam, ParsedResponse + + +from gen_ai_hub.proxy.core import get_proxy_client +from gen_ai_hub.proxy.core.base import BaseProxyClient +from gen_ai_hub.proxy.core.utils import NOT_GIVEN, NotGiven, if_set, kwargs_if_set + +DEFAULT_API_VERSION = '2025-03-01-preview' # https://learn.microsoft.com/en-us/azure/ai-foundry/openai/api-version-lifecycle?tabs=key#api-evolution + +# Model name patterns +COHERE_MODEL_PATTERN = r"^cohere--" +O_SERIES_MODEL_PATTERN = r"^o\d+" +GPT_5_MODEL_PATTERN = r"^gpt-5(-mini|-nano)?$" +COHERE_REASONING_MODEL_PATTERN = r"^cohere--command-.*-reasoning$" + +ResponseFormatT = TypeVar('ResponseFormatT') +_current_deployment = contextvars.ContextVar('current_deployment') + + +@contextmanager +def set_deployment(value): + """Context manager to set the current deployment. + + :param value: The deployment to set as current. + :type value: Deployment + """ + token = _current_deployment.set(value) + try: + yield + finally: + _current_deployment.reset(token) + + +def get_current_deployment(): + """Get the current deployment from the context variable. + + :return: The current deployment. + :rtype: Deployment + """ + return _current_deployment.get(None) + + +class Embeddings(Embeddings_): + """ + A class that represents the Embeddings. It extends the Embeddings_ class + and provides functionality to create embeddings based on the provided input. + """ + + def create(self, + *, + input: Union[str, List[str], List[int], List[List[int]], None], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Embedding: + """Creates embeddings based on the provided input and model information. + + For NVIDIA models, use extra_body to specify additional parameters: + extra_body={'input_type': 'query'|'passage'} + + :param input: the input data for which embeddings are to be created. + :type input: Union[str, List[str], List[int], List[List[int]], None] + :param model:the model to use for creating embeddings, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the ID of the deployment to use, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the name of the model to use, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the ID of the config to use, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the name of the config to use, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :param kwargs: additional keyword arguments. + :type kwargs: dict + :raises ValueError: if the deployment cannot be selected or the model name is not provided. + :return: the created embeddings. + :rtype: Embedding + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + + with set_deployment(deployment): + return super().create(input=input, model=model_name, **kwargs) + + +class AsyncEmbeddings(AsyncEmbeddings_): + """ + The AsyncEmbeddings class is a subclass of AsyncEmbeddings_. This class is used for creating + embeddings asynchronously. It provides an interface for fetching embeddings of a given input + from a selected deployment on a proxy client. + """ + + async def create(self, + *, + input: Union[str, List[str], List[int], List[List[int]], None], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Embedding: + """Asynchronously creates embeddings for the given input using a specific model. + + :param input: the input data for which embeddings are to be created. + :type input: Union[str, List[str], List[int], List[List[int]], None] + :param model: the model to use for creating embeddings, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the ID of the deployment to use, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the name of the model to use, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the ID of the config to use, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the name of the config to use, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the created embeddings. + :rtype: Embedding + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + + with set_deployment(deployment): + return await super().create(input=input, model=model_name, **kwargs) + + +class Completions(Completions_): + """ + The Completions class is a subclass of Completions_. It provides a way to create a completion given a prompt and + certain other configurations. It extends from the base class Completions_ and overrides the create method to cater + to the specific requirements. + """ + + def create(self, + *, + prompt: Union[str, List[str], List[int], List[List[int]], None], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Completion | Stream[Completion]: + """This method creates a completion based on the provided parameters. It uses a proxy client to select a + deployment and then calls the create method of the parent class to generate a completion. + + :param prompt: the input prompt(s) for the completion. + :type prompt: Union[str, List[str], List[int], List[List[int]], None] + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the completion or stream of completions created based on the provided prompt. + :rtype: Completion | Stream[Completion] + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return super().create(prompt=prompt, model=model_name, **kwargs) + + +class AsyncCompletions(AsyncCompletions_): + """ + AsyncCompletions is a subclass of AsyncCompletions_. It provides a way to create a completion given a prompt and + certain other configurations in asynchronous way. It extends from the base class Completions_ and overrides the + create method to cater to the specific requirements. + """ + + async def create(self, + *, + prompt: Union[str, List[str], List[int], List[List[int]], None], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Completion | Stream[Completion]: + """Asynchronously creates a completion or a stream of completions based on the given prompt and + other parameters. + + :param prompt: the input prompt(s) for the completion. + :type prompt: Union[str, List[str], List[int], List[List[int]], None] + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the completion or stream of completions created based on the provided prompt. + :rtype: Completion | Stream[Completion] + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return await super().create(prompt=prompt, model=model_name, **kwargs) + + +class Chat(Chat_): + """A class that handles chat completions, extending from the class 'Chat_'.""" + + def __init__(self, client: OpenAI) -> None: + """Initializes the Chat class with the provided OpenAI client. + + :param client: The OpenAI client to be used for chat completions. + :type client: OpenAI + """ + super().__init__(client) + self.completions = ChatCompletions(client) + + +class ChatCompletions(ChatCompletions_): + """ + A class that handles chat completions, extending from the class 'ChatCompletions_'. + """ + + def _prepare_chat(self, config_id, config_name, deployment_id, kwargs, model, model_name, model_version): + """ + Prepares the deployment and model name for the create and parse completion request. + + Args: + config_id: The configuration ID to use for chat completion + config_name: The configuration name to use for chat completion + deployment_id: The deployment ID to use for chat completion + kwargs: Keyword arguments dictionary that may be modified + model: The model to use for chat completion + model_name: The model name to use for chat completion, + model_version: The model version to use for chat completion + + Returns: + tuple: (deployment, model_name) prepared for the request + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + # Reasoning models do not support temperature + if not self.supports_temperature(model_name) and 'temperature' in kwargs: + kwargs.pop('temperature') + # Cohere models do not support the 'n' and 'max_completion_tokens' parameters + if model_name and re.search(COHERE_MODEL_PATTERN, model_name): + kwargs.pop('n', None) + kwargs.pop('max_completion_tokens', None) + return deployment, model_name + + def create(self, + *, + messages: List[ChatCompletionMessageParam], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> ChatCompletion: + """Creates a chat completion using the provided parameters. + + :param messages: the list of chat completion message parameters. + :type messages: List[ChatCompletionMessageParam] + :param model: the model to use for chat completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment ID to use for chat completion, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name to use for chat completion, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version to use for chat completion, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration ID to use for chat completion, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name to use for chat completion, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the chat completion created with the provided parameters. + :rtype: ChatCompletion + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + return super().create(messages=messages, model=model_name, **kwargs) + + def parse(self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + response_format: type[ResponseFormatT] | NotGiven = NOT_GIVEN, + **kwargs) -> ParsedChatCompletion[ResponseFormatT]: + """Parses chat completions using the provided parameters and returns a ParsedChatCompletion object. + This method provides richer integrations with Python specific types by converting pydantic models + into JSON schemas and parsing the response content back into the given model. + + :param messages: the list of chat completion message parameters. + :type messages: Iterable[ChatCompletionMessageParam] + :param model: the model to use for chat completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment ID to use for chat completion, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name to use for chat completion, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version to use for chat completion, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration ID to use for chat completion, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name to use for chat completion, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :param response_format: the response format type for structured output, defaults to NOT_GIVEN + :type response_format: type[ResponseFormatT] | NotGiven, optional + :return: the parsed chat completion with the structured response. + :rtype: ParsedChatCompletion[ResponseFormatT] + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + return super().parse(messages=messages, model=model_name, response_format=response_format, **kwargs) + + @staticmethod + def supports_temperature(model_name: str) -> bool: + """Checks if the given model supports the `temperature` parameter. + Reasoning models do not support temperature e.g., o1[-mini], o3[-mini], 5[-mini, -nano], + cohere--command-a-reasoning + + :param model_name: the name of the model to check. + :type model_name: str + :return: True if the model supports `temperature`, False otherwise + :rtype: bool + """ + return (not re.search(O_SERIES_MODEL_PATTERN, model_name) + and not re.search(GPT_5_MODEL_PATTERN, model_name) + and not re.search(COHERE_REASONING_MODEL_PATTERN, model_name)) + + +class Responses(Responses_): + """ + The Responses class is a subclass of Responses_. It provides a way to create a response for the given input and + certain other configurations. It extends from the base class Responses_ and overrides the create method to + cater to the specific requirements. + """ + + def _prepare_chat(self, config_id, config_name, deployment_id, kwargs, model, model_name, model_version): + """ + Prepares the deployment and model name for the create and parse responses request. + + Args: + config_id: The configuration ID to use for chat completion + config_name: The configuration name to use for chat completion + deployment_id: The deployment ID to use for chat completion + kwargs: Keyword arguments dictionary that may be modified + model: The model to use for chat completion + model_name: The model name to use for chat completion, + model_version: The model version to use for chat completion + + Returns: + tuple: (deployment, model_name) prepared for the request + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + # Reasoning models do not support temperature + if not self.supports_temperature(model_name) and 'temperature' in kwargs: + kwargs.pop('temperature') + # Cohere models do not support the 'n' and 'max_completion_tokens' parameters + if model_name and re.search(COHERE_MODEL_PATTERN, model_name): + kwargs.pop('n', None) + kwargs.pop('max_completion_tokens', None) + return deployment, model_name + + def create(self, + *, + input: str | ResponseInputParam | Omit = None, + instructions: str | Omit = None, + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Response | Stream[ResponseStreamEvent]: + """This method creates a response based on the provided parameters. It uses a proxy client to select a + deployment and then calls the create method of the parent class to generate a response. + + :param input: Text, image, or file inputs to the model, used to generate a response, defaults to NOT_GIVEN + :type input: str | ResponseInputParam | None | NotGiven, optional + :param instructions: A system (or developer) message inserted into the model's context, defaults to NOT_GIVEN + :type instructions: str | None | NotGiven, optional + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the response or stream of responsess created based on the provided input. + :rtype: Response | Stream[ResponseStreamEvent]: + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return super().create(instructions=instructions, input=input, model=model_name, **kwargs) + + def parse( + self, + *, + input: str | ResponseInputParam | Omit = None, + instructions: str | Omit = None, + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> ParsedResponse[TextFormatT]: + """Parses responses using the provided parameters and returns a ParsedResponse object. + This method provides richer integrations with Python specific types by converting pydantic models + into JSON schemas and parsing the response content back into the given model + + :param input: Text, image, or file inputs to the model, used to generate a response, defaults to NOT_GIVEN + :type input: str | ResponseInputParam | None | NotGiven, optional + :param instructions: A system (or developer) message inserted into the model's context, defaults to NOT_GIVEN + :type instructions: str | None | NotGiven, optional + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: ParsedResponse object + :rtype: ParsedResponse + """ + + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return super().parse(instructions=instructions, input=input, model=model_name, **kwargs) + + @staticmethod + def supports_temperature(model_name: str) -> bool: + """Checks if the given model supports the `temperature` parameter. + Reasoning models do not support temperature e.g., o1[-mini], o3[-mini], 5[-mini, -nano], + cohere--command-a-reasoning + + :param model_name: the name of the model to check. + :type model_name: str + :return: True if the model supports `temperature`, False otherwise + :rtype: bool + """ + return (not re.search(O_SERIES_MODEL_PATTERN, model_name) + and not re.search(GPT_5_MODEL_PATTERN, model_name) + and not re.search(COHERE_REASONING_MODEL_PATTERN, model_name)) + + +class AsyncChat(AsyncChat_): + """A class that handles asynchronous chat completions, extending from the class 'AsyncChat_'.""" + + def __init__(self, client: OpenAI) -> None: + """Initializes the AsyncChat class with the provided OpenAI client. + + :param client: The OpenAI client to be used for chat completions. + :type client: OpenAI + """ + super().__init__(client) + self.completions = AsyncChatCompletions(client) + + +class AsyncChatCompletions(AsyncChatCompletions_): + """ + The AsyncChatCompletions class is a derived class which extends AsyncChatCompletions_. + This class is used to handle asynchronous chat completion requests. It provides methods + to create and manage chat completions in an asynchronous manner. + """ + + def _prepare_chat(self, config_id, config_name, deployment_id, kwargs, model, model_name, model_version): + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + + # Reasoning models do not support temperature + if not self.supports_temperature(model_name) and 'temperature' in kwargs: + kwargs.pop('temperature') + # Cohere models do not support the 'n' and 'max_completion_tokens' parameters + if model_name and re.search(COHERE_MODEL_PATTERN, model_name): + kwargs.pop('n', None) + kwargs.pop('max_completion_tokens', None) + + return deployment, model_name + + async def create(self, + *, + messages: List[ChatCompletionMessageParam], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> ChatCompletion: + """Asynchronously creates a new chat completion. + + :param messages: the list of chat completion message parameters. + :type messages: List[ChatCompletionMessageParam] + :param model: the model to be used, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the created chat completion. + :rtype: ChatCompletion + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + return await super().create(messages=messages, model=model_name, **kwargs) + + async def parse(self, + *, + messages: Iterable[ChatCompletionMessageParam], + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + response_format: type[ResponseFormatT] | NotGiven = NOT_GIVEN, + **kwargs) -> ParsedChatCompletion[ResponseFormatT]: + """Asynchronously parses chat completions using the provided parameters and + returns a ParsedChatCompletion object. + This method provides richer integrations with Python specific types by converting pydantic models + into JSON schemas and parsing the response content back into the given model. + + :param messages: the list of chat completion message parameters. + :type messages: Iterable[ChatCompletionMessageParam] + :param model: the model to use for chat completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment ID to use for chat completion, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name to use for chat completion, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version to use for chat completion, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration ID to use for chat completion, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name to use for chat completion, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :param response_format: the response format type for structured output, defaults to NOT_GIVEN + :type response_format: type[ResponseFormatT] | NotGiven, optional + :return: the parsed chat completion with the structured response. + :rtype: ParsedChatCompletion[ResponseFormatT] + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + return await super().parse(messages=messages, model=model_name, response_format=response_format, **kwargs) + + @staticmethod + def supports_temperature(model_name: str) -> bool: + """Checks if the given model supports the `temperature` parameter. + Reasoning models do not support temperature e.g., o1[-mini], o3[-mini], cohere--command-a-reasoning + + :param model_name: the name of the model to check. + :type model_name: str + :return: True if the model supports `temperature`, False otherwise + :rtype: bool + """ + return (not re.search(O_SERIES_MODEL_PATTERN, model_name) + and not re.search(GPT_5_MODEL_PATTERN, model_name) + and not re.search(COHERE_REASONING_MODEL_PATTERN, model_name)) + + +class AsyncResponses(AsyncResponses_): + """ + The asynch Responses class is a subclass of AsyncResponses_. + It provides a way to create a response for the given input and certain other configurations. + It extends from the base class AsyncResponses_ and overrides the create method to cater to the specific + requirements. + """ + + def _prepare_chat(self, config_id, config_name, deployment_id, kwargs, model, model_name, model_version): + """ + Prepares the deployment and model name for the create and parse responses request. + + Args: + config_id: The configuration ID to use for chat completion + config_name: The configuration name to use for chat completion + deployment_id: The deployment ID to use for chat completion + kwargs: Keyword arguments dictionary that may be modified + model: The model to use for chat completion + model_name: The model name to use for chat completion, + model_version: The model version to use for chat completion + + Returns: + tuple: (deployment, model_name) prepared for the request + """ + proxy_client: BaseProxyClient = self._client.proxy_client + model_name = if_set(model_name, if_set(model)) + model_identification = kwargs_if_set( + deployment_id=deployment_id, + model_name=model_name, + model_version=model_version, + config_id=config_id, + config_name=config_name, + ) + deployment = proxy_client.select_deployment(**model_identification) + model_name = deployment.model_name or '???' + # Reasoning models do not support temperature + if not self.supports_temperature(model_name) and 'temperature' in kwargs: + kwargs.pop('temperature') + # Cohere models do not support the 'n' and 'max_completion_tokens' parameters + if model_name and re.search(COHERE_MODEL_PATTERN, model_name): + kwargs.pop('n', None) + kwargs.pop('max_completion_tokens', None) + return deployment, model_name + + async def create(self, + *, + input: str | ResponseInputParam | Omit = None, + instructions: str | Omit = None, + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> Response | AsyncStream[ResponseStreamEvent]: + """Async method that creates a response based on the provided parameters. It uses a proxy client to select a + deployment and then calls the create method of the parent class to generate a response. + + :param input: Text, image, or file inputs to the model, used to generate a response, defaults to NOT_GIVEN + :type input: str | ResponseInputParam | None | NotGiven, optional + :param instructions: A system (or developer) message inserted into the model's context, defaults to NOT_GIVEN + :type instructions: str | None | NotGiven, optional + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: the response or stream of responsess created based on the provided input. + :rtype: Response | AsyncStream[ResponseStreamEvent]: + """ + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return await super().create(instructions=instructions, input=input, model=model_name, **kwargs) + + async def parse( + self, + *, + input: str | ResponseInputParam | Omit = None, + instructions: str | Omit = None, + model: str | None | NotGiven = NOT_GIVEN, + deployment_id: str | None | NotGiven = NOT_GIVEN, + model_name: str | None | NotGiven = NOT_GIVEN, + model_version: str | None | NotGiven = NOT_GIVEN, + config_id: str | None | NotGiven = NOT_GIVEN, + config_name: str | None | NotGiven = NOT_GIVEN, + **kwargs) -> ParsedResponse[TextFormatT]: + """Async parses responses using the provided parameters and returns a ParsedResponse object. + This method provides richer integrations with Python specific types by converting pydantic models + into JSON schemas and parsing the response content back into the given model + + :param input: Text, image, or file inputs to the model, used to generate a response, defaults to NOT_GIVEN + :type input: str | ResponseInputParam | None | NotGiven, optional + :param instructions: A system (or developer) message inserted into the model's context, defaults to NOT_GIVEN + :type instructions: str | None | NotGiven, optional + :param model: the model to be used for the completion, defaults to NOT_GIVEN + :type model: str | None | NotGiven, optional + :param deployment_id: the deployment id, defaults to NOT_GIVEN + :type deployment_id: str | None | NotGiven, optional + :param model_name: the model name, defaults to NOT_GIVEN + :type model_name: str | None | NotGiven, optional + :param model_version: the model version, defaults to NOT_GIVEN + :type model_version: str | None | NotGiven, optional + :param config_id: the configuration id, defaults to NOT_GIVEN + :type config_id: str | None | NotGiven, optional + :param config_name: the configuration name, defaults to NOT_GIVEN + :type config_name: str | None | NotGiven, optional + :return: ParsedResponse object + :rtype: ParsedResponse + """ + + deployment, model_name = self._prepare_chat(config_id, config_name, deployment_id, kwargs, model, model_name, + model_version) + + with set_deployment(deployment): + kwargs.pop("root_client", None) + kwargs.pop("root_async_client", None) + return await super().parse(instructions=instructions, input=input, model=model_name, **kwargs) + + @staticmethod + def supports_temperature(model_name: str) -> bool: + """Checks if the given model supports the `temperature` parameter. + Reasoning models do not support temperature e.g., o1[-mini], o3[-mini], 5[-mini, -nano], + cohere--command-a-reasoning + + :param model_name: the name of the model to check. + :type model_name: str + :return: True if the model supports `temperature`, False otherwise + :rtype: bool + """ + return (not re.search(O_SERIES_MODEL_PATTERN, model_name) + and not re.search(GPT_5_MODEL_PATTERN, model_name) + and not re.search(COHERE_REASONING_MODEL_PATTERN, model_name)) + +class OpenAIWithRawResponse: + """ + This class is a wrapper for the OpenAI API client that provides raw responses. + Note: The properties 'edits', 'files', 'images', 'audio', 'moderations', 'models', + 'fine_tuning', 'fine_tunes' and 'beta' are placeholders and currently do not provide any + functionality. + + Attributes: + completions: An instance of CompletionsWithRawResponse class. + + chat: An instance of ChatWithRawResponse class. + + edits: Not currently used. + + embeddings: An instance of EmbeddingsWithRawResponse class if client.embeddings is not None. + + files: Not currently used. + + images: Not currently used. + + audio: Not currently used. + + moderations: Not currently used. + + models: Not currently used. + + fine_tuning: Not currently used. + + fine_tunes: Not currently used. + + beta: Not currently used. + + The class is designed to provide the raw responses from OpenAI's API endpoints. It currently supports completions, + chat, and embeddings endpoints. + """ + + def __init__(self, client: OpenAI) -> None: + """Initializes the OpenAIWithRawResponse class with the provided OpenAI client. + + :param client: An instance of OpenAI client. + :type client: OpenAI + """ + self.completions = resources.CompletionsWithRawResponse(client.completions) + self.chat = resources.ChatWithRawResponse(client.chat) + self.edits = None + self.embeddings = resources.EmbeddingsWithRawResponse(client.embeddings) if client.embeddings else None + self.files = None + self.images = None + self.audio = None + self.moderations = None + self.models = None + self.fine_tuning = None + self.fine_tunes = None + self.beta = self # required for structure_outputs when using langchain + + +class AsyncOpenAIWithRawResponse: + """ + A class that provides an asynchronous interface to the OpenAI API, returning raw responses. + + This class wraps the core functionality of OpenAI's API, offering access to completions, + chat capabilities, and embeddings. It is designed to work with OpenAI's asynchronous client, + allowing for concurrent requests to the API. + + Note: The properties 'edits', 'files', 'images', 'audio', 'moderations', 'models', + 'fine_tuning', 'fine_tunes' and 'beta' are placeholders and currently do not provide any + functionality. + + Attributes: + completions: An instance of `resources.AsyncCompletionsWithRawResponse` for managing completions with the API. + + chat: An instance of `resources.AsyncChatWithRawResponse` for managing chat with the API. + + embeddings: An instance of `resources.AsyncEmbeddingsWithRawResponse` for managing embeddings with the API. + + edits: Currently a placeholder with no functionality. + + files: Currently a placeholder with no functionality. + + images: Currently a placeholder with no functionality. + + audio: Currently a placeholder with no functionality. + + moderations: Currently a placeholder with no functionality. + + models: Currently a placeholder with no functionality. + + fine_tuning: Currently a placeholder with no functionality. + + fine_tunes: Currently a placeholder with no functionality. + + beta: Currently a placeholder with no functionality. + """ + + def __init__(self, client: AsyncOpenAI) -> None: + """Initializes the AsyncOpenAIWithRawResponse class with the provided AsyncOpenAI client. + + :param client: An instance of AsyncOpenAI client. + :type client: AsyncOpenAI + """ + self.completions = resources.AsyncCompletionsWithRawResponse(client.completions) + self.chat = resources.AsyncChatWithRawResponse(client.chat) + self.edits = None + self.embeddings = resources.AsyncEmbeddingsWithRawResponse(client.embeddings) + self.files = None + self.images = None + self.audio = None + self.moderations = None + self.models = None + self.fine_tuning = None + self.fine_tunes = None + self.beta = None + + +def _prepare_url(url: str) -> httpx.URL: + deployment = get_current_deployment() + prediction_url = deployment.prediction_url + if prediction_url: + return httpx.URL(prediction_url) + + url = httpx.URL(url) + if url.is_relative_url: + deployment_url = httpx.URL(get_current_deployment().url.rstrip('/') + '/') + url = deployment_url.raw_path + url.raw_path.lstrip(b"/") + return deployment_url.copy_with(raw_path=url) + return url + + +class OpenAI(OpenAI_): + """ + This is a class for the OpenAI API client. It is designed to handle various services provided by OpenAI such as text + completions, chat, embeddings etc. + + Attributes: + proxy_client (BaseProxyClient, optional): An instance of a Proxy Client. Defaults to None. + + api_version (str, optional): API version used for OpenAI API calls. Defaults to DEFAULT_API_VERSION. + + completions (Completions): An instance of the Completions class for text generation. + + chat (Chat): An instance of the Chat class for conversation. + + edits: Placeholder for future use. Currently set to None. + + embeddings (Embeddings): An instance of the Embeddings class for getting text embeddings. + + files: Placeholder for future use. Currently set to None. + + images: Placeholder for future use. Currently set to None. + + audio: Placeholder for future use. Currently set to None. + + moderations: Placeholder for future use. Currently set to None. + + models: Placeholder for future use. Currently set to None. + + fine_tuning: Placeholder for future use. Currently set to None. + + fine_tunes: Placeholder for future use. Currently set to None. + + beta: Placeholder for future use. Currently set to None. + + with_raw_response (OpenAIWithRawResponse): An instance of the OpenAIWithRawResponse class for returning raw + responses from the API. + """ + + def __init__(self, + *, + proxy_client: Optional[BaseProxyClient] = None, + api_version: Optional[str] = DEFAULT_API_VERSION, + **kwargs) -> None: + """Initializes the OpenAI API client with the provided parameters. + + :param proxy_client: An instance of a Proxy Client. Defaults to None. + :type proxy_client: Optional[BaseProxyClient], optional + :param api_version: API version used for OpenAI API calls. Defaults to DEFAULT_API_VERSION. + :type api_version: Optional[str], optional + """ + self.proxy_client = proxy_client or get_proxy_client() + for kwarg in ('api_key', 'organization', 'base_url'): + kwargs.pop(kwarg, None) + default_query = {'api-version': api_version or DEFAULT_API_VERSION, **kwargs.pop('default_query', {})} + super().__init__(api_key='???', base_url='???', organization='???', default_query=default_query, **kwargs) + + self.completions = Completions(self) + self.chat = Chat(self) + self.edits = None + self.embeddings = Embeddings(self) + self.files = None + self.images = None + self.audio = None + self.moderations = None + self.models = None + self.fine_tuning = None + self.fine_tunes = None + self.beta = self # required for structure_outputs when using langchain + self.with_raw_response = OpenAIWithRawResponse(self) + self.responses = Responses(self) + + @property + def default_headers(self) -> dict[str, str | Omit]: + headers = super().default_headers + headers.update(self.proxy_client.request_header) + return headers + + def _prepare_url(self, url: str) -> httpx.URL: + return _prepare_url(url) + + def request(self, cast_to, options, *args, **kwargs): + options.json_data.update(get_current_deployment().additional_request_body_kwargs()) + return super().request(cast_to, options, *args, **kwargs) + + +class AsyncOpenAI(AsyncOpenAI_): + """ + An async version of the OpenAI API client. + + This class is used to interact with the OpenAI API asynchronously. It supports various operations like creating + completions, generating chat messages, and getting embeddings. + + Attributes: + proxy_client (BaseProxyClient): A proxy client to make API requests. If not provided, a default one will be + created. + + api_version (str, optional): The version of the OpenAI API to use. Default is defined by DEFAULT_API_VERSION. + + completions (AsyncCompletions): A client for interacting with the OpenAI API's completions. + + chat (AsyncChat): A client for interacting with the OpenAI API's chat. + + edits (None): Placeholder for future support of "edits" operations. + + embeddings (AsyncEmbeddings): A client for interacting with the OpenAI API's embeddings. + + files (None): Placeholder for future support of "files" operations. + + images (None): Placeholder for future support of "images" operations. + + audio (None): Placeholder for future support of "audio" operations. + + moderations (None): Placeholder for future support of "moderations" operations. + + models (None): Placeholder for future support of "models" operations. + + fine_tuning (None): Placeholder for future support of "fine_tuning" operations. + + fine_tunes (None): Placeholder for future support of "fine_tunes" operations. + beta (None): Placeholder for future support of "beta" operations. + with_raw_response (AsyncOpenAIWithRawResponse): A client that returns raw API responses. + """ + + def __init__(self, + *, + proxy_client: Optional[BaseProxyClient] = None, + api_version: Optional[str] = DEFAULT_API_VERSION, + **kwargs) -> None: + """Initializes the AsyncOpenAI client with the provided parameters. + + :param proxy_client: An instance of a Proxy Client. Defaults to None. + :type proxy_client: Optional[BaseProxyClient], optional + :param api_version: API version used for OpenAI API calls. Defaults to DEFAULT_API_VERSION. + :type api_version: Optional[str], optional + """ + self.proxy_client = proxy_client or get_proxy_client() + for kwarg in ('api_key', 'organization', 'base_url'): + kwargs.pop(kwarg, None) + default_query = {'api-version': api_version or DEFAULT_API_VERSION, **kwargs.pop('default_query', {})} + super().__init__(api_key='???', base_url='???', organization='???', default_query=default_query, **kwargs) + + self.completions = AsyncCompletions(self) + self.chat = AsyncChat(self) + self.edits = None + self.embeddings = AsyncEmbeddings(self) + self.files = None + self.images = None + self.audio = None + self.moderations = None + self.models = None + self.fine_tuning = None + self.fine_tunes = None + self.beta = self # required for structure_outputs when using langchain + self.with_raw_response = AsyncOpenAIWithRawResponse(self) + self.responses = AsyncResponses(self) + + @property + def default_headers(self) -> dict[str, str | Omit]: + headers = super().default_headers + headers.update(self.proxy_client.request_header) + return headers + + def _prepare_url(self, url: str) -> httpx.URL: + return _prepare_url(url) + + def request(self, cast_to, options, *args, **kwargs): + """Overrides the request method to include additional request body kwargs from the current deployment. + + :param cast_to: the type to cast the response to. + :type cast_to: any + :param options: the request options. + :type options: any + :return: the response from the request. + :rtype: CoroutineType[Any, Any, ResponseT@request] + """ + options.json_data.update(get_current_deployment().additional_request_body_kwargs()) + return super().request(cast_to, options, *args, **kwargs) diff --git a/packages/gen/gen_ai_hub/proxy/native/sap/__init__.py b/packages/gen/gen_ai_hub/proxy/native/sap/__init__.py new file mode 100644 index 0000000..61ec934 --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/sap/__init__.py @@ -0,0 +1,6 @@ +from .client import RPTClient +from .models import (TargetColumn, RPTRequest, RPTResponse, RPTException, ResponseStatus, ResponseMetadata, Prediction, + PredictionItem, PredictionConfig, ErrorResponseDetails, DataType) + +__all__ = ['RPTClient', 'TargetColumn', 'RPTRequest', 'RPTResponse', 'RPTException', 'ResponseStatus', + 'ResponseMetadata', 'Prediction', 'PredictionItem', 'PredictionConfig', 'ErrorResponseDetails', 'DataType'] \ No newline at end of file diff --git a/packages/gen/gen_ai_hub/proxy/native/sap/client.py b/packages/gen/gen_ai_hub/proxy/native/sap/client.py new file mode 100644 index 0000000..45345cf --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/sap/client.py @@ -0,0 +1,286 @@ +from typing import Optional, Union +import httpx + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.proxy.native.sap.models import RPTResponse, RPTException, RPTRequest + +PREDICTION_SUFFIX = "/predict" + +def _handle_http_error(error, response: httpx.Response): + + if not response.content: + raise error + try: + error_status = response.json().get("status", {}) + error_detail = response.json().get("detail", None) + except ValueError as exc: + raise error from exc + raise RPTException( + status=error_status, + detail=error_detail, + ) from error + + +class RPTClient: + """ + Handles interaction with RPT models for making predictions. + + This class acts as a client for executing prediction requests using RPT + models deployed via the Gen AI Hub. It retrieves deployment information, + handles timeouts, and processes request and response data. + + :param proxy_client: Proxy client for interacting with the Gen AI Hub API. + If not provided, a default implementation is used. + :type proxy_client: Optional[GenAIHubProxyClient] + + :param timeout: Default timeout value for the HTTP client used for requests. + :type timeout: Union[int, float, httpx.Timeout, None] + """ + + def __init__( + self, + proxy_client: Optional[GenAIHubProxyClient] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + ): + self.proxy_client = proxy_client or get_proxy_client(proxy_version="gen-ai-hub") + self.timeout = timeout + self.client = httpx.Client(timeout=self.timeout) + self.async_client = httpx.AsyncClient(timeout=self.timeout) + + def _determine_timeout(self, timeout: httpx.Timeout) -> httpx.Timeout: + # Determine the timeout to use for this request + if timeout is not None: + # Overwrite default timeout for this request + request_timeout = timeout + elif self.timeout is not None: + # Use the default timeout is set + request_timeout = self.timeout + else: + # If timeout is not set, use httpx client's default behavior, rather than "None" (disables timeout) + request_timeout = httpx.USE_CLIENT_DEFAULT + return request_timeout + + def _execute_request( + self, + body: RPTRequest, + api_url: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> RPTResponse: + """ + Executes an HTTP POST request to a prediction API endpoint with the given + body, headers, URL, and timeout configuration. + + This method serializes the given request body to JSON and sends it to the + constructed API endpoint. + + :param body: The request object containing the necessary data to be sent + to the prediction API. + :type body: RPTRequest + + :param api_url: The base URL of the API to which the request is made. + :type api_url: str + + :param timeout: The timeout configuration for the HTTP request. Can be an + integer, float, httpx.Timeout object, or None. + :type timeout: Union[int, float, httpx.Timeout, None] + + :returns: A response object representing the data returned from the API + after successful execution. + :rtype: RPTResponse + + :raises RPTException: If the response contains any HTTP status errors. + """ + + response = self.client.post( + api_url + PREDICTION_SUFFIX, + headers=self.proxy_client.request_header, + json=body.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return RPTResponse(**data) + + async def _a_execute_request( + self, + body: RPTRequest, + api_url: str, + timeout: Union[int, float, httpx.Timeout, None] = None, + ) -> RPTResponse: + """ + Asynchronously executes an HTTP POST request to a prediction API endpoint + with the given body, headers, URL, and timeout configuration. + + This method serializes the given request body to JSON and sends it to the + constructed API endpoint. + + :param body: The request object containing the necessary data to be sent + to the prediction API. + :type body: RPTRequest + + :param api_url: The base URL of the API to which the request is made. + :type api_url: str + + :param timeout: The timeout configuration for the HTTP request. Can be an + integer, float, httpx.Timeout object, or None. + :type timeout: Union[int, float, httpx.Timeout, None] + + :returns: A response object representing the data returned from the API + after successful execution. + :rtype: RPTResponse + + :raises RPTException: If the response contains any HTTP status errors. + """ + + response = await self.async_client.post( + api_url + PREDICTION_SUFFIX, + headers=self.proxy_client.request_header, + json=body.model_dump(), + timeout=self._determine_timeout(timeout) + ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as error: + _handle_http_error(error, response) + + data = response.json() + return RPTResponse(**data) + + def _get_url(self, model_name: Optional[str] = None, model_version: Optional[str] = None, **kwargs) -> str: + """ + Builds a URL for the selected deployment based on the provided model + name and/or deployment ID. + + This function queries a proxy client to select a deployment that matches + the specified criteria (model name and/or deployment ID) and retrieves + the deployment's URL. If no matching deployment is found, an exception + is raised. + + :param model_name: The name of the model to filter deployments. + :type model_name: Optional[str] + :param model_version: The version of the model to filter deployments. + :type model_version: Optional[str] + + :param kwargs: Additional keyword arguments for identifying the deployment. + :type kwargs: dict + + :returns: The URL of the selected deployment. + :rtype: str + + :raises ValueError: If no deployment matches the provided parameters. + """ + filters = kwargs.copy() + if model_name: + filters["model_name"] = model_name + if model_version: + filters["model_version"] = model_version + try: + url = self.proxy_client.select_deployment(**filters).url + except ValueError: + raise ValueError(f"No deployment found for the given parameters:{filters}.") + return url + + def _validate_request_body(self, body: Union[dict, RPTRequest]): + if isinstance(body, dict): + return RPTRequest(**body) + return body + + def _validate_parameters(self, model_name: Optional[str] = None, model_version: Optional[str] = None, + deployment_url: Optional[str] = None, **kwargs): + if not model_name and not deployment_url and not kwargs: + raise ValueError("Deployment URL or model_name or other deployment parameters must be provided.") + if model_version and not model_name: + raise ValueError("model_version can be provided only if model_name is provided.") + + def predict(self, + body: Union[dict, RPTRequest], + deployment_url: Optional[str] = None, + model_name: Optional[str] = None, + model_version: Optional[str] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + **kwargs) -> RPTResponse: + """ + Executes a prediction request by sending the provided data and deployment parameters. + + The `body` parameter can be supplied either as a dictionary or as an instance of `RPTRequest`. + + :param body: The input data for the prediction request, represented either as a + dictionary or an `RPTRequest` object. + :type body: Union[dict, RPTRequest] + + :param deployment_url: The URL of the deployment to use for prediction. If not provided, + `model_name` or other deployment parameters must be specified. + :type deployment_url: Optional[str] + + :param model_name: The name of the model to use for prediction. If not provided, + `api_url` or other deployment parameters must be specified. + :type model_name: Optional[str] + :param model_version: The version of the model to use for prediction. + Could be provided only if `model_name` is provided. + :type model_version: Optional[str] + :param timeout: The time duration to wait for the prediction request to complete. + Can be an integer, float, or an instance of `httpx.Timeout`. + :type timeout: Union[int, float, httpx.Timeout, None] + + :returns: The response received from the prediction endpoint, represented as an + `RPTResponse` object. + :rtype: RPTResponse + + :raises ValueError: If no deployment is found for the given parameters. + """ + body = self._validate_request_body(body) + self._validate_parameters(model_name, model_version, deployment_url, **kwargs) + return self._execute_request(body=body, + api_url = deployment_url if deployment_url else self._get_url( + model_name, model_version, **kwargs), + timeout=timeout) + + async def apredict(self, + body: Union[dict, RPTRequest], + deployment_url: Optional[str] = None, + model_name: Optional[str] = None, + model_version: Optional[str] = None, + timeout: Union[int, float, httpx.Timeout, None] = None, + **kwargs) -> RPTResponse: + """ + Asynchronously executes a prediction request by sending the provided data and deployment parameters. + + The `body` parameter can be supplied either as a dictionary or as an instance of `RPTRequest`. + + :param body: The input data for the prediction request, represented either as a + dictionary or an `RPTRequest` object. + :type body: Union[dict, RPTRequest] + + :param deployment_url: The URL of the deployment to use for prediction. If not provided, + `model_name` or other deployment parameters must be specified. + :type deployment_url: Optional[str] + + :param model_name: The name of the model to use for prediction. If not provided, + `api_url` or other deployment parameters must be specified. + :type model_name: Optional[str] + + :param model_version: The version of the model to use for prediction. + Could be provided only if `model_name` is provided. + :type model_version: Optional[str] + + :param timeout: The time duration to wait for the prediction request to complete. + Can be an integer, float, or an instance of `httpx.Timeout`. + :type timeout: Union[int, float, httpx.Timeout, None] + + :returns: The response received from the prediction endpoint, represented as an + `RPTResponse` object. + :rtype: RPTResponse + + :raises ValueError: If no deployment is found for the given parameters. + """ + body = self._validate_request_body(body) + self._validate_parameters(model_name, model_version, deployment_url, **kwargs) + return await self._a_execute_request(body=body, + api_url = deployment_url if deployment_url else self._get_url( + model_name, model_version, **kwargs), + timeout=timeout) diff --git a/packages/gen/gen_ai_hub/proxy/native/sap/models.py b/packages/gen/gen_ai_hub/proxy/native/sap/models.py new file mode 100644 index 0000000..7770adc --- /dev/null +++ b/packages/gen/gen_ai_hub/proxy/native/sap/models.py @@ -0,0 +1,208 @@ +from typing import Optional, Literal, Union, Any +from pydantic import BaseModel, RootModel, model_validator + + +class TargetColumn(BaseModel): + """Represents a target column in data. + + :param name: Name of the target column. + :type name: str + :param prediction_placeholder: Placeholder string denoting where predictions will be inserted. + Defaults to ``"[PREDICT]"``. + :type prediction_placeholder: str + :param task_type: Task type of the target column. + One of ``"classification"`` or ``"regression"``. Defaults to ``None``. + :type task_type: Optional[Literal["classification", "regression"]] + """ + + name: str + prediction_placeholder: str = "[PREDICT]" + task_type: Optional[Literal["classification", "regression"]] = None + + +class PredictionConfig(BaseModel): + """ + The configuration object specifying which columns to predict + + :param target_columns: List of target columns to predict. + :type target_columns: list[TargetColumn] + """ + + target_columns: list[TargetColumn] + + +class DataType(BaseModel): + """Schema definition for a column. + + :param dtype: The data type of the column. + :type dtype: Literal["string", "numeric", "date"] + """ + + dtype: Literal["string", "numeric", "date"] + + +class RPTRequest(BaseModel): + """Request model for predictions. + + Provide exactly one of ``rows`` or ``columns``. + + :param prediction_config: Configuration describing what to predict. + :type prediction_config: PredictionConfig + :param index_column: Name of a column used to identify the row. This column is not used + as an input feature and may be returned in the response objects. + :type index_column: Optional[str] + :param rows: Array of objects representing table rows (both context and query rows). + :type rows: Optional[list[dict]] + :param columns: Mapping from column name to array of column values. + :type columns: Optional[dict[str, list]] + :param data_schema: Schema definition for all columns, e.g. + ``{"columnA": {"dtype": "string"}, "columnB": {"dtype": "numeric"}}``. + :type data_schema: Optional[dict[str, DataType]] + :param parse_data_types: Relevant when ``data_schema`` is not provided. Whether to parse data types + (e.g., interpret strings as numbers or dates). Defaults to ``True``. + :type parse_data_types: bool + """ + + prediction_config: PredictionConfig + index_column: Optional[str] = None + rows: Optional[list[dict]] = None + columns: Optional[dict[str, list]] = None + data_schema: Optional[dict[str, DataType]] = None + parse_data_types: bool = True + + @model_validator(mode="after") + def validate_rows_xor_columns(self): + """Validate that exactly one of ``rows`` or ``columns`` is provided. + + :raises ValueError: If neither or both of ``rows`` and ``columns`` are provided. + :return: The validated request instance. + :rtype: RPTRequest + """ + if (self.rows is None) == (self.columns is None): + raise ValueError("Exactly one of 'rows' or 'columns' must be provided.") + return self + + def model_dump(self, **kwargs): + """Serialize the model to a dictionary. + + Ensures the non-provided alternative (``rows`` or ``columns``) is omitted from the dump + and that ``None`` values are excluded. + + :param kwargs: Keyword arguments forwarded to ``pydantic.BaseModel.model_dump``. + :type kwargs: Any + :return: Serialized dictionary representation of the model. + :rtype: dict + """ + kwargs.setdefault("exclude_none", True) + return super().model_dump(**kwargs) + + +class ResponseMetadata(BaseModel): + """Response metadata. + + :param num_rows: Total number of input rows. + :type num_rows: int + :param num_columns: Total number of input columns. + :type num_columns: int + :param num_predictions: Number of table cells containing the specified placeholder values, + summed over all target columns. + :type num_predictions: int + :param num_query_rows: Number of query rows for which a prediction was made. + :type num_query_rows: int + """ + + num_rows: int + num_columns: int + num_predictions: int + num_query_rows: int + + +class ResponseStatus(BaseModel): + """Status information for a prediction request. + + :param code: Numeric status code. + :type code: int + :param message: Status message. + :type message: str + """ + + code: int + message: str + + +class PredictionItem(BaseModel): + """Single prediction result. + + :param prediction: The predicted value. + :type prediction: Union[str, float] + :param confidence: Confidence score for classification tasks. Defaults to ``None``. + :type confidence: Optional[float] + """ + + prediction: Union[str, float] + confidence: Optional[float] = None + + +class Prediction(RootModel[dict[str, Union[list[PredictionItem], Any]]]): + """Container for prediction results keyed by target column name.""" + + def __getitem__(self, key): + """Return the prediction payload for ``key``. + + :param key: Prediction key to access. + :type key: str + :return: Value associated with ``key``. + :rtype: Any + """ + return self.root[key] + + +class RPTResponse(BaseModel): + """Response model for an RPT request. + + :param id: Unique identifier for the response. + :type id: str + :param status: Status describing the outcome of the request. + :type status: ResponseStatus + :param predictions: Prediction data returned by the service. + :type predictions: list[Prediction] + :param metadata: Metadata about the request/response. + :type metadata: ResponseMetadata + """ + + id: str + status: ResponseStatus + predictions: list[Prediction] + metadata: ResponseMetadata + + +class ErrorResponseDetails(BaseModel): + """Details of an error response. + + :param loc: Location in the request where the error occurred. + :type loc: list + :param msg: Human-readable error message. + :type msg: str + :param type: Error category/type. + :type type: str + """ + + loc: list + msg: str + type: str + + +class RPTException(Exception): + """Exception representing an error response from the RPT service. + + :param status: Status indicating the error category/type. + :type status: ResponseStatus + :param detail: Optional list of additional error details. + :type detail: Optional[list[ErrorResponseDetails]] + """ + def __init__(self, + status: ResponseStatus, + detail: Optional[list[ErrorResponseDetails]] = None + ): + self.status = status + self.detail = detail diff --git a/packages/gen/integration_tests/README_BEDROCK_SEPARATION.md b/packages/gen/integration_tests/README_BEDROCK_SEPARATION.md new file mode 100644 index 0000000..995fde4 --- /dev/null +++ b/packages/gen/integration_tests/README_BEDROCK_SEPARATION.md @@ -0,0 +1,134 @@ +# Bedrock Model Separation Guide + +This document explains how to separate Bedrock models from standard models in your integration tests. + +## Overview + +The `setup_aicore.py` module now provides separate setup functions and mixins to handle different model types: + +- **Bedrock models**: Amazon and Anthropic models available via AWS Bedrock +- **Standard models**: All other models (OpenAI, Google, IBM, etc.) + +## Available Functions + +### Setup Functions + +1. **`setup_bedrock_models(client)`** - Sets up only Bedrock models +2. **`setup_standard_models(client)`** - Sets up only standard (non-Bedrock) models +3. **`setup_aicore_instance(client)`** - Sets up all models (backward compatibility) + +### Helper Functions + +- **`get_bedrock_models()`** - Returns list of Bedrock model tuples +- **`get_standard_models()`** - Returns list of standard model tuples + +### Test Mixins + +1. **`TestCaseAICoreSetupMixin`** - Sets up all models (original behavior) +2. **`TestCaseBedrockSetupMixin`** - Sets up only Bedrock models +3. **`TestCaseStandardSetupMixin`** - Sets up only standard models + +## Usage Examples + +### For Bedrock Tests (marked with @pytest.mark.bedrock) + +```python +import pytest +from integration_tests.setup_aicore import TestCaseBedrockSetupMixin + +@pytest.mark.bedrock +class TestAmazonServices(TestCaseBedrockSetupMixin, unittest.TestCase): + def test_bedrock_functionality(self): + # This test will only have Bedrock models deployed + # Access via self.aicore_deployments + pass +``` + +### For Standard Tests + +```python +from integration_tests.setup_aicore import TestCaseStandardSetupMixin + +class TestOpenAIServices(TestCaseStandardSetupMixin, unittest.TestCase): + def test_openai_functionality(self): + # This test will only have standard models deployed + # Access via self.aicore_deployments + pass +``` + +### For Mixed Tests (Backward Compatible) + +```python +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + +class TestAllServices(TestCaseAICoreSetupMixin, unittest.TestCase): + def test_mixed_functionality(self): + # This test will have all models deployed (original behavior) + # Access via self.aicore_deployments + pass +``` + +## Model Categories + +### Bedrock Models +- `amazon--titan-embed-image` +- `amazon--titan-embed-text` +- `amazon--nova-micro` +- `amazon--nova-lite` +- `amazon--nova-pro` +- `amazon--nova-premier` +- `anthropic--claude-3-haiku` +- `anthropic--claude-4-opus` +- `anthropic--claude-3.5-sonnet` +- `anthropic--claude-3.7-sonnet` +- `anthropic--claude-4-sonnet` + +### Standard Models +- OpenAI models (`gpt-4o`, `gpt-4o-mini`, etc.) +- Google models (`gemini-2.0-flash`, etc.) +- Mistral models (`mistralai--mistral-small-instruct`, etc.) +- Embedding models (`text-embedding-3-small`, etc.) + +## Running Tests + +### Run only Bedrock tests +```bash +pytest -m bedrock +``` + +### Run only non-Bedrock tests +(credentials for US10 intprod required - see in main README.md section "Running Bedrock Tests") +```bash +pytest -m "not bedrock" +``` + +### Run all tests +```bash +pytest +``` + +## Benefits + +1. **Faster Test Execution**: Bedrock tests only deploy Bedrock models +2. **Resource Optimization**: Reduced deployment overhead for specific test suites +3. **Environment Separation**: Clear separation between Bedrock and standard environments +4. **Backward Compatibility**: Existing tests continue to work unchanged +5. **Targeted Testing**: Easier to run specific test suites in different environments + +## Migration Guide + +To migrate existing Bedrock tests: + +1. Import the new mixin: + ```python + from integration_tests.setup_aicore import TestCaseBedrockSetupMixin + ``` + +2. Replace `TestCaseAICoreSetupMixin` with `TestCaseBedrockSetupMixin` for classes marked with `@pytest.mark.bedrock`: + ```python + @pytest.mark.bedrock + class TestBedrock(TestCaseBedrockSetupMixin, unittest.TestCase): # Changed here + # Your test methods remain the same + ``` + +3. No changes needed to test methods - they'll continue to access `self.aicore_deployments` as before. diff --git a/packages/gen/integration_tests/__init__.py b/packages/gen/integration_tests/__init__.py new file mode 100644 index 0000000..b5bec85 --- /dev/null +++ b/packages/gen/integration_tests/__init__.py @@ -0,0 +1,5 @@ +import random + +def get_random_string(l=4): + alphanumeric = 'abcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(random.choices(alphanumeric, k=l)) diff --git a/packages/gen/integration_tests/batch_service/__init__.py b/packages/gen/integration_tests/batch_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/batch_service/requests.jsonl b/packages/gen/integration_tests/batch_service/requests.jsonl new file mode 100644 index 0000000..c27a693 --- /dev/null +++ b/packages/gen/integration_tests/batch_service/requests.jsonl @@ -0,0 +1,2 @@ +{"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4.1", "messages": [{"role": "user", "content": "What is machine learning?"}], "max_tokens": 150}} +{"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4.1", "messages": [{"role": "user", "content": "Explain neural networks in simple terms"}], "max_tokens": 150}} diff --git a/packages/gen/integration_tests/batch_service/test_async.py b/packages/gen/integration_tests/batch_service/test_async.py new file mode 100644 index 0000000..e508deb --- /dev/null +++ b/packages/gen/integration_tests/batch_service/test_async.py @@ -0,0 +1,75 @@ +""" +Integration tests for the BatchService client — async methods. + +Mirrors test_service.py but exercises the acreate / alist / aget / +aget_status / acancel / adelete variants end-to-end. +""" + +import unittest + +from gen_ai_hub.batch_service import BatchDeleteResponse +from gen_ai_hub.batch_service.exceptions import BatchServiceError +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, +) +from integration_tests.batch_service.test_base import BatchServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + +@retry_on_429_or_503_class() +class TestBatchServiceAsync(BatchServiceTestBase, unittest.IsolatedAsyncioTestCase): + + created_batch_id: str | None = None + + async def asyncTearDown(self): + if self.created_batch_id: + try: + await self.service.acancel(self.created_batch_id) + await self.service.adelete(self.created_batch_id) + except Exception: + pass + self.created_batch_id = None + + async def test_acreate_returns_pending_job(self): + create_resp = await self.service.acreate( + input_uri=self.input_uri, + output_uri=self.output_uri, + provider=self.provider, + model=self.model, + ) + self.created_batch_id = create_resp.id + + self.assertIsInstance(create_resp, BatchCreateResponse) + self.assertIsNotNone(create_resp.id) + self.assertIn(create_resp.status, ("PENDING", "RUNNING")) + + detail_resp = await self.service.aget(create_resp.id) + self.assertIsInstance(detail_resp, BatchDetailResponse) + self.assertEqual(detail_resp.id, create_resp.id) + + status_resp = await self.service.aget_status(create_resp.id) + self.assertIsInstance(status_resp, BatchStatusResponse) + self.assertIsNotNone(status_resp.current_status) + + list_resp = await self.service.alist() + self.assertIsInstance(list_resp, BatchListResponse) + self.assertIsNotNone(list_resp.count) + + cancel_resp = await self.service.acancel(create_resp.id) + self.assertIsInstance(cancel_resp, BatchCancelResponse) + self.assertEqual(cancel_resp.id, create_resp.id) + + delete_resp = await self.service.adelete(self.created_batch_id) + self.assertIsInstance(delete_resp, BatchDeleteResponse) + self.assertEqual(delete_resp.id, create_resp.id) + + self.created_batch_id = None # tearDown should not re-cancel + + async def test_aget_nonexistent_raises_error(self): + fake_id = "00000000-0000-0000-0000-000000000000" + with self.assertRaises(BatchServiceError) as ctx: + await self.service.aget(fake_id) + self.assertEqual(ctx.exception.status_code, 404) diff --git a/packages/gen/integration_tests/batch_service/test_base.py b/packages/gen/integration_tests/batch_service/test_base.py new file mode 100644 index 0000000..282aa30 --- /dev/null +++ b/packages/gen/integration_tests/batch_service/test_base.py @@ -0,0 +1,64 @@ +""" +Shared base for batch service integration tests. + +Provides a BatchService instance already wired to the live AI Core backend. +The service URL is resolved once (via the proxy client's base_url) and shared +across all test classes in the session. +""" + +import os +import unittest + +from time import sleep + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.batch_service.models.response import BatchStatus +from gen_ai_hub.batch_service.service import BatchService + +AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" +AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" +AWS_BUCKET_NAME = "AWS_BUCKET_NAME" +AWS_HOST = "AWS_HOST" +AWS_REGION = "AWS_REGION" +AICORE_RESOURCE_GROUP = "AICORE_RESOURCE_GROUP" + + +class BatchServiceTestBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.proxy_client: GenAIHubProxyClient = get_proxy_client('gen-ai-hub') + cls.service = BatchService(proxy_client=cls.proxy_client) + cls.secret_name = 'batch-service-oss' + cls.input_uri = f"ai://{cls.secret_name}/batch_service_test_data/requests.jsonl" + cls.output_uri = f"ai://{cls.secret_name}/batch_service_test_data/output/" + cls.provider = "azure-openai" + cls.model = "gpt-4.1" + cls.create_object_store_secret() + + def wait_for_batch_to_be_deletable(self, batch_id: str, timeout=300, polling_interval=5): + status = None + deletable_statuses = [BatchStatus.COMPLETED, BatchStatus.FAILED, BatchStatus.CANCELLED] + while status not in deletable_statuses and timeout != 0: + sleep(polling_interval) + response = self.service.get_status(batch_id=batch_id) + status = response.current_status + timeout -= polling_interval + + @classmethod + def create_object_store_secret(cls): + aws_access_key_id = os.environ.get(AWS_ACCESS_KEY_ID) + aws_secret_access_key = os.environ.get(AWS_SECRET_ACCESS_KEY) + response = cls.proxy_client.ai_core_client.object_store_secrets.create( + name=cls.secret_name, + type="S3", + data={ + AWS_ACCESS_KEY_ID: aws_access_key_id, + AWS_SECRET_ACCESS_KEY: aws_secret_access_key, + }, + bucket=os.environ.get(AWS_BUCKET_NAME), + endpoint=os.environ.get(AWS_HOST), + region=os.environ.get(AWS_REGION), + resource_group=os.environ.get(AICORE_RESOURCE_GROUP), + ) + diff --git a/packages/gen/integration_tests/batch_service/test_service.py b/packages/gen/integration_tests/batch_service/test_service.py new file mode 100644 index 0000000..559e45b --- /dev/null +++ b/packages/gen/integration_tests/batch_service/test_service.py @@ -0,0 +1,111 @@ +""" +Integration tests for the BatchService client — synchronous methods. + +These tests exercise the full lifecycle of a batch job against the live +SAP AI Core batch service endpoint: + create → get → get_status → cancel → delete + +Each test class that mutates state is responsible for cleaning up its job +in tearDown so the resource group stays tidy. + +All tests are wrapped with the retry decorator to handle transient 429/503 +responses from the live service. +""" + +import unittest + +from gen_ai_hub.batch_service import BatchDeleteResponse +from gen_ai_hub.batch_service.exceptions import BatchServiceError +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, +) +from integration_tests.batch_service.test_base import BatchServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestBatchServiceCreate(BatchServiceTestBase): + """Tests for create and basic read-back of a newly created batch job.""" + + created_batch_id: str | None = None + + def tearDown(self): + if self.created_batch_id: + try: + self.service.cancel(self.created_batch_id) + self.service.delete(self.created_batch_id) + except Exception: + pass + self.created_batch_id = None + + def test_create_and_get_returns_detail(self): + create_resp = self.service.create( + type="llm-native", + input_uri=self.input_uri, + output_uri=self.output_uri, + provider=self.provider, + model=self.model, + ) + self.assertIsInstance(create_resp, BatchCreateResponse) + self.assertIsNotNone(create_resp.id) + self.assertIn(create_resp.status, ("PENDING", "RUNNING")) + self.created_batch_id = create_resp.id + + + detail = self.service.get(create_resp.id) + self.assertIsInstance(detail, BatchDetailResponse) + self.assertEqual(detail.id, create_resp.id) + self.assertIsNotNone(detail.status) + self.assertIsNotNone(detail.input) + self.assertIsNotNone(detail.output) + + status = self.service.get_status(create_resp.id) + self.assertIsInstance(status, BatchStatusResponse) + self.assertIsNotNone(status.current_status) + self.assertIsNotNone(status.target_status) + + list_resp = self.service.list() + self.assertIsInstance(list_resp, BatchListResponse) + self.assertTrue(list_resp.count >= 1) + for item in list_resp.resources: + self.assertIsNotNone(item.id) + self.assertIsNotNone(item.status) + + cancel_resp = self.service.cancel(create_resp.id) + self.assertIsInstance(cancel_resp, BatchCancelResponse) + self.assertEqual(cancel_resp.id, create_resp.id) + self.assertIsNotNone(cancel_resp.message) + + self.wait_for_batch_to_be_deletable(create_resp.id) + + delete_resp = self.service.delete(self.created_batch_id) + self.assertIsInstance(delete_resp, BatchDeleteResponse) + self.assertEqual(delete_resp.id, create_resp.id) + + def test_get_nonexistent_job_raises_batch_service_error(self): + fake_id = "00000000-0000-0000-0000-000000000000" + with self.assertRaises(BatchServiceError) as ctx: + self.service.get(fake_id) + self.assertEqual(ctx.exception.status_code, 404) + + def test_get_status_nonexistent_job_raises_batch_service_error(self): + fake_id = "00000000-0000-0000-0000-000000000000" + with self.assertRaises(BatchServiceError) as ctx: + self.service.get_status(fake_id) + self.assertEqual(ctx.exception.status_code, 404) + + def test_cancel_nonexistent_job_raises_batch_service_error(self): + fake_id = "00000000-0000-0000-0000-000000000000" + with self.assertRaises(BatchServiceError) as ctx: + self.service.cancel(fake_id) + self.assertEqual(ctx.exception.status_code, 404) + + def test_delete_nonexistent_job_raises_batch_service_error(self): + fake_id = "00000000-0000-0000-0000-000000000000" + with self.assertRaises(BatchServiceError) as ctx: + self.service.delete(fake_id) + self.assertEqual(ctx.exception.status_code, 404) diff --git a/packages/gen/integration_tests/constants.py b/packages/gen/integration_tests/constants.py new file mode 100644 index 0000000..053ece9 --- /dev/null +++ b/packages/gen/integration_tests/constants.py @@ -0,0 +1,26 @@ +# The models below are used for testing purposes in the integration tests. +# The models are chosen based on the following criteria +# - to cover a variety of functionalities and capabilities across different providers, and +# - to save costs by using smaller models where possible, and +# - to ensure that the latest models are used for testing. + +AMAZON_NOVA_MICRO_TEST_MODEL = "amazon--nova-micro" +AMAZON_NOVA_PREMIER_TEST_MODEL = "amazon--nova-premier" +AMAZON_TITAN_EMBEDDING_TEST_MODEL = "amazon--titan-embed-text" +CLAUDE_4_5_SONNET_TEST_MODEL = "anthropic--claude-4.5-sonnet" +CLAUDE_4_5_HAIKU_TEST_MODEL = "anthropic--claude-4.5-haiku" +GEMINI_2_5_FLASH_LITE_TEST_MODEL = "gemini-2.5-flash-lite" +GOOGLE_EMBEDDING_TEST_MODEL = "gemini-embedding" +NVIDIA_EMBEDDING_TEST_MODEL = "nvidia--llama-3.2-nv-embedqa-1b" +OPENAI_EMBEDDING_TEST_MODEL = "text-embedding-3-small" +OPENAI_GPT_4O_MINI_TEST_MODEL = "gpt-4o-mini" +OPENAI_GPT_O3_MINI_TEST_MODEL = "o3-mini" +OPENAI_GPT_O4_MINI_TEST_MODEL = "o4-mini" +OPENAI_GPT_5_TEST_MODEL_NANO = "gpt-5-nano" +OPENAI_GPT_5_TEST_MODEL = "gpt-5" +OPENAI_GPT_5_MINI_TEST_MODEL = "gpt-5-mini" +MISTRAL_TEST_MODEL = "mistralai--mistral-small-instruct" +PERPLEXITY_TEST_MODEL = "sonar" +PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL = "sonar-deep-research" +COHERE_COMMAND_A_TEST_MODEL = "cohere--command-a-reasoning" +SAP_RPT_1_SMALL_TEST_MODEL = "sap-rpt-1-small" diff --git a/packages/gen/integration_tests/document_grounding/__init__.py b/packages/gen/integration_tests/document_grounding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/document_grounding/test_pipeline_api_client.py b/packages/gen/integration_tests/document_grounding/test_pipeline_api_client.py new file mode 100644 index 0000000..bb63e5b --- /dev/null +++ b/packages/gen/integration_tests/document_grounding/test_pipeline_api_client.py @@ -0,0 +1,102 @@ +import unittest +import time +from typing import cast + +import requests.status_codes +from ai_api_client_sdk.exception import AIAPIServerException +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.document_grounding.client import PipelineAPIClient +from gen_ai_hub.document_grounding.models.pipeline import ( + PipelineIdResponse, + CommonConfiguration, + S3PipelineCreateRequest, + GetPipelinesResponse, + BasePipelineResponse, + GetPipelineStatusResponse, + SearchPipelineRequest, + SearchPipelinesResponse, + ManualPipelineTrigger, + GetPipelineExecutionsResponse, +) +from gen_ai_hub.proxy import get_proxy_client +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestPipelinesAPIIntegration(unittest.TestCase): + """ + Test the document-grounding API: pipelines section + + Prerequisites: + see https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-resource-group-for-ai-data-management?q=document%20grounding&locale=en-US + - Create or patch a resource group with the label for activating the document-grounding API. + - Generic secrets for S3 storage should be created for the resource group. + """ + + @classmethod + def _create_pipeline(cls): + s3_config = S3PipelineCreateRequest(configuration=CommonConfiguration(destination="s3-secret-test-grounding")) + return cls.client.create_pipeline(s3_config) + + @classmethod + def setUpClass(cls): + cls.proxy_client = cast(GenAIHubProxyClient, get_proxy_client()) + cls.client = PipelineAPIClient(proxy_client=cls.proxy_client) + pipeline_id = cls._create_pipeline().pipelineId + cls.pipeline_ids = [pipeline_id] + + @classmethod + def tearDownClass(cls): + for pipeline_id in cls.pipeline_ids: + try: + cls.client.delete_pipeline_by_id(pipeline_id) + except AIAPIServerException as e: + if e.status_code != 404: + raise e + + def test_create_pipeline_s3(self): + """Test creating a pipeline for documents in S3 storage.""" + + response = self._create_pipeline() + self.pipeline_ids.append(response.pipelineId) + self.assertIsInstance(response, PipelineIdResponse) + self.assertIsNotNone(response.pipelineId) + + def test_get_pipelines(self): + """Test retrieving pipelines.""" + response = self.client.get_pipelines() + self.assertIsInstance(response, GetPipelinesResponse) + self.assertGreaterEqual(response.count, 1) + + def test_get_pipeline_by_id(self): + """Test retrieving a pipeline by ID.""" + pipeline_id = self.pipeline_ids[0] + response = self.client.get_pipeline_by_id(pipeline_id) + self.assertEqual(response.id, pipeline_id) + + def test_get_pipeline_status(self): + """Test retrieving the status of a pipeline.""" + pipeline_id = self.pipeline_ids[0] + response = self.client.get_pipeline_status(pipeline_id) + self.assertIsInstance(response, GetPipelineStatusResponse) + + def test_delete_pipeline_by_id(self): + """Test deleting a pipeline by ID.""" + new_pipeline_id = self._create_pipeline().pipelineId + self.pipeline_ids.append(new_pipeline_id) + # wait for the pipeline to be created + time.sleep(3) + response = self.client.delete_pipeline_by_id(new_pipeline_id) + self.assertEqual(response.status_code, requests.status_codes.codes.NO_CONTENT, msg=response.text) + self.pipeline_ids.pop(-1) + + def test_search_pipelines(self): + """Test searching pipelines.""" + request = SearchPipelineRequest( + dataRepositoryMetadata=[ + {"key": "description", "value": ["details"]} + ] + ) + response = self.client.search_pipelines(request) + self.assertIsInstance(response, SearchPipelinesResponse) + self.assertGreaterEqual(response.count, 0) \ No newline at end of file diff --git a/packages/gen/integration_tests/document_grounding/test_retrieval_api_client.py b/packages/gen/integration_tests/document_grounding/test_retrieval_api_client.py new file mode 100644 index 0000000..c73f89d --- /dev/null +++ b/packages/gen/integration_tests/document_grounding/test_retrieval_api_client.py @@ -0,0 +1,193 @@ +import unittest +from typing import cast +import requests + +from .. import get_random_string +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.document_grounding.clients.retrieval_api_client import RetrievalAPIClient +from gen_ai_hub.document_grounding.clients.vector_api_client import VectorAPIClient +from gen_ai_hub.document_grounding.models.retrieval import ( + RetrievalSearchInput, + RetrievalSearchFilter, + RetrievalSearchConfiguration, + RetrievalSearchDocumentKeyValueListPair, + DataRepositories, + DataRepository, + RetrievalSearchResults, + RetrievalSearchResults, + RetrievalPerFilterSearchResult, + RetrievalDataRepositorySearchResult, + DataRepositoryWithDocuments, + RetrievalDocument, + RetrievalChunk, +) +from gen_ai_hub.document_grounding.models.vector import ( + CollectionCreateRequest, + EmbeddingConfig, + VectorKeyValueListPair, + BaseDocument, + TextOnlyBaseChunk, + DocumentsCreateRequest, +) +from gen_ai_hub.proxy import get_proxy_client +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestRetrievalAPIIntegration(unittest.TestCase): + """ + Integration test suite for the Retrieval API. + + Focuses on: + - Data repositories listing and details + - Retrieval search across vector repositories + + Prerequisites: + - Valid access configuration. + """ + + @classmethod + def setUpClass(cls): + """Prepare test data — create a vector collection and document for retrieval testing.""" + cls.proxy_client = cast(GenAIHubProxyClient, get_proxy_client()) + cls.vector_client = VectorAPIClient(proxy_client=cls.proxy_client) + cls.retrieval_client = RetrievalAPIClient(proxy_client=cls.proxy_client) + + # --- Create test vector collection --- + cls.collection_title = f"retrieval-integration-test-collection-{get_random_string()}" + create_request = CollectionCreateRequest( + title=cls.collection_title, + embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-large"), + metadata=[ + VectorKeyValueListPair(key="purpose", value=["retrieval-test"]), + VectorKeyValueListPair(key="source", value=["integration"]), + ], + ) + create_resp = cls.vector_client.create_collection(create_request) + assert create_resp.status_code == requests.status_codes.codes.ACCEPTED + + list_resp = cls.vector_client.get_collections() + assert list_resp.resources + for res in list_resp.resources: + if res.title == cls.collection_title: + cls.collection_id = res.id + + # --- Create test document --- + doc = BaseDocument( + metadata=[VectorKeyValueListPair(key="url", value=["http://retrieval-test.com"])], + chunks=[ + TextOnlyBaseChunk( + content="Joule is an AI copilot that helps automate SAP workflows.", + metadata=[VectorKeyValueListPair(key="index", value=["1"])], + ), + TextOnlyBaseChunk( + content="It understands enterprise context and enhances productivity.", + metadata=[VectorKeyValueListPair(key="index", value=["2"])], + ), + ], + ) + create_doc_resp = cls.vector_client.create_documents(cls.collection_id, DocumentsCreateRequest(documents=[doc])) + cls.document_id = create_doc_resp.documents[0].id + + + @classmethod + def tearDownClass(cls): + """Clean up the created vector collection and document.""" + cls.vector_client.delete_document(cls.collection_id, cls.document_id) + + cls.vector_client.delete_collection(cls.collection_id) + + + def test_01_get_data_repositories(self): + """Verify that the test collection appears as a repository in Retrieval API.""" + response = self.retrieval_client.get_data_repositories() + self.assertIsInstance(response, DataRepositories) + created_repo = None + for r in response.resources: + if r.id == self.__class__.collection_id: + created_repo = r + break + + self.assertIsNotNone(created_repo) + self.assertEqual(created_repo.title, self.collection_title) + self.assertEqual(created_repo.type, "vector") + + metadata = {m.key: m.value for m in created_repo.metadata} + self.assertEqual(metadata["purpose"], ["retrieval-test"]) + self.assertEqual(metadata["source"], ["integration"]) + + help_repo = None + for r in response.resources: + if r.type == "help.sap.com": + help_repo = r + break + + self.assertIsNotNone(help_repo) + self.assertEqual(help_repo.title, "SAP Help Portal - help.sap.com") + self.assertEqual(help_repo.metadata, []) + self.assertEqual(help_repo.type, "help.sap.com") + + def test_02_get_data_repository_by_id(self): + """Get repository details and verify consistency with the created collection.""" + response = self.retrieval_client.get_data_repository_by_id(self.__class__.collection_id) + + self.assertIsInstance(response, DataRepository) + + self.assertEqual(response.id, self.__class__.collection_id) + self.assertEqual(response.title, self.collection_title) + self.assertEqual(response.type, "vector") + + metadata = {m.key: m.value for m in response.metadata} + self.assertIn("purpose", metadata) + self.assertIn("source", metadata) + self.assertEqual(metadata["purpose"], ["retrieval-test"]) + self.assertEqual(metadata["source"], ["integration"]) + + def test_03_search_retrieval(self): + """Perform a retrieval search in the created vector repository and verify content consistency.""" + search_input = RetrievalSearchInput( + query="What is Joule?", + filters=[ + RetrievalSearchFilter( + id="string", + dataRepositoryType="vector", + searchConfiguration=RetrievalSearchConfiguration(), + dataRepositories=[self.__class__.collection_id], + documentMetadata=[ + RetrievalSearchDocumentKeyValueListPair( + key="url", + value=["http://retrieval-test.com"], + selectMode=["ignoreIfKeyAbsent"], + ) + ], + ) + ], + ) + + response = self.retrieval_client.search(search_input) + + self.assertIsInstance(response, RetrievalSearchResults) + self.assertTrue(response.results) + + filter_result = response.results[0] + self.assertEqual(filter_result.filterId, "string") + self.assertTrue(filter_result.results) + + repo = filter_result.results[0].dataRepository + self.assertEqual(repo.id, self.__class__.collection_id) + self.assertEqual(repo.title, self.collection_title) + + metadata = {m.key: m.value for m in repo.metadata} + self.assertEqual(metadata.get("purpose"), ["retrieval-test"]) + self.assertEqual(metadata.get("source"), ["integration"]) + + document = repo.documents[0] + metadata_dict = {m.key: m.value for m in document.metadata} + self.assertEqual(metadata_dict.get("url"), ["http://retrieval-test.com"]) + self.assertTrue(document.chunks) + + chunk = document.chunks[0] + self.assertIn("Joule", chunk.content) + + chunk_metadata = {m.key: m.value for m in chunk.metadata} + self.assertIn("index", chunk_metadata) diff --git a/packages/gen/integration_tests/document_grounding/test_vector_api_client.py b/packages/gen/integration_tests/document_grounding/test_vector_api_client.py new file mode 100644 index 0000000..9f7541e --- /dev/null +++ b/packages/gen/integration_tests/document_grounding/test_vector_api_client.py @@ -0,0 +1,319 @@ +import unittest +from typing import cast +import requests.status_codes + +from .. import get_random_string +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.proxy import get_proxy_client +from gen_ai_hub.document_grounding.clients.vector_api_client import VectorAPIClient +from gen_ai_hub.document_grounding.models.vector import ( + Collection, + CollectionCreateRequest, + EmbeddingConfig, + VectorKeyValueListPair, + DocumentsCreateRequest, + BaseDocument, + TextOnlyBaseChunk, + DocumentsUpdateRequest, + Document, + TextSearchRequest, + VectorSearchFilter, + VectorSearchConfiguration, + VectorSearchDocumentKeyValueListPair, +) +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestVectorAPIIntegration(unittest.TestCase): + """ + Integration test suite for the Vector API. + + Covers: + - Collections + - Collection statuses + - Documents CRUD + - Search queries + + Prerequisites: + - Resource group with Document Grounding API enabled. + - Valid Vector Store and secrets. + """ + + @classmethod + def setUpClass(cls): + cls.proxy_client = cast(GenAIHubProxyClient, get_proxy_client()) + cls.client = VectorAPIClient(proxy_client=cls.proxy_client) + cls.collection_id = None + cls.document_id = None + + + def test_integration(self): + # --- Collections --- + + # Create a vector collection. + self.collection_title = f"test-canary-collection-{get_random_string()}" + request = CollectionCreateRequest( + title=self.collection_title, + embeddingConfig=EmbeddingConfig(modelName="text-embedding-3-large"), + metadata=[ + VectorKeyValueListPair(key="purpose", value=["demonstration"]), + VectorKeyValueListPair(key="a-random-key", value=["hello world!"]), + ], + ) + response = self.client.create_collection(request) + + self.assertEqual(response.status_code, requests.status_codes.codes.ACCEPTED) + + # Retrieve all collections and validate the last created collection data. + response = self.client.get_collections() + self.assertGreaterEqual(response.count, 0) + + self.assertTrue(response.resources) + + test_collection: Collection = None + for res in response.resources: + if res.title == self.collection_title: + test_collection = res + self.assertIsNotNone(test_collection) + self.__class__.collection_id = test_collection.id + + self.assertIn(self.collection_title, test_collection.title) + + self.assertEqual( + test_collection.embeddingConfig.modelName, + "text-embedding-3-large", + ) + + metadata_keys = {m.key: m.value for m in (test_collection.metadata or [])} + self.assertIn("purpose", metadata_keys) + self.assertIn("a-random-key", metadata_keys) + + self.assertEqual(metadata_keys["purpose"], ["demonstration"]) + self.assertEqual(metadata_keys["a-random-key"], ["hello world!"]) + + self.assertTrue(self.__class__.collection_id) + + # Fetch details of a specific collection by ID and verify all expected fields. + self.assertTrue(self.__class__.collection_id) + + response = self.client.get_collection_by_id(self.__class__.collection_id) + + self.assertEqual(response.id, self.__class__.collection_id) + self.assertEqual(response.title, self.collection_title) + self.assertEqual(response.embeddingConfig.modelName, "text-embedding-3-large") + + expected_metadata = { + "purpose": ["demonstration"], + "a-random-key": ["hello world!"] + } + + metadata_dict = {m.key: m.value for m in response.metadata} + + for key, expected_value in expected_metadata.items(): + self.assertIn(key, metadata_dict) + self.assertEqual(metadata_dict[key], expected_value) + + # --- Collection statuses --- + + # Check async creation status (CREATED/PENDING). + self.assertTrue(self.__class__.collection_id) + response = self.client.get_collection_creation_status(self.__class__.collection_id) + + self.assertTrue(response.status) + + + # --- Documents --- + + # Insert new documents into the collection and verify creation response. + self.assertTrue(self.__class__.collection_id) + + document = BaseDocument( + metadata=[VectorKeyValueListPair(key="url", value=["http://hello.com", "123"])], + chunks=[ + TextOnlyBaseChunk( + content=( + "Joule is the AI copilot that truly understands your business. " + "Joule revolutionizes how you interact with your SAP systems." + ), + metadata=[VectorKeyValueListPair(key="index", value=["1"])], + ), + TextOnlyBaseChunk( + content=( + "It enables the Intelligent Enterprise, guiding you through SAP content discovery " + "and providing transparent access to relevant processes." + ), + metadata=[VectorKeyValueListPair(key="index", value=["2"])], + ), + ], + ) + + request = DocumentsCreateRequest(documents=[document]) + response = self.client.create_documents(self.__class__.collection_id, request) + + self.assertTrue(response.documents) + created_doc = response.documents[0] + + self.assertIsNotNone(created_doc.id) + self.assertEqual(len(created_doc.metadata), 1) + + metadata = created_doc.metadata[0] + self.assertEqual(metadata.key, "url") + self.assertEqual(metadata.value, ["http://hello.com", "123"]) + + self.__class__.document_id = created_doc.id + + # Retrieve all documents in a collection and verify their structure. + self.assertTrue(self.__class__.collection_id) + + response = self.client.get_documents(self.__class__.collection_id) + + self.assertGreaterEqual(response.count or 0, 1) + self.assertTrue(response.resources) + + for doc in response.resources: + self.assertIsNotNone(doc.id) + self.assertTrue(doc.metadata) + metadata = doc.metadata[0] + self.assertEqual(metadata.key, "url") + self.assertEqual(metadata.value, ["http://hello.com", "123"]) + + # Get a document by its ID and verify all content and metadata. + self.assertTrue(self.__class__.collection_id) + self.assertTrue(self.__class__.document_id) + + response = self.client.get_document_by_id(self.__class__.collection_id, self.__class__.document_id) + + self.assertEqual(response.id, self.__class__.document_id) + self.assertTrue(response.metadata) + self.assertTrue(response.chunks) + self.assertGreaterEqual(len(response.chunks), 2) + + metadata = {m.key: m.value for m in response.metadata} + self.assertIn("url", metadata) + self.assertEqual(metadata["url"], ["http://hello.com", "123"]) + + first_chunk = response.chunks[0] + second_chunk = response.chunks[1] + + self.assertIn("Joule is the AI copilot", first_chunk.content) + self.assertIn("It enables the Intelligent Enterprise", second_chunk.content) + + chunk_metadata_keys = [m.key for m in first_chunk.metadata] + self.assertIn("index", chunk_metadata_keys) + + # Update an existing document and verify that changes persist. + self.assertTrue(self.__class__.collection_id) + self.assertTrue(self.__class__.document_id) + + updated_doc = Document( + id=self.__class__.document_id, + metadata=[VectorKeyValueListPair(key="url", value=["http://hello1.com"])], + chunks=[ + TextOnlyBaseChunk( + content=( + "Joule is not the AI copilot that truly understands your business. " + "Joule revolutionizes how you interact with your SAP business systems, making every touchpoint count and every task simpler." + ), + metadata=[VectorKeyValueListPair(key="index", value=["1"])], + ), + TextOnlyBaseChunk( + content=( + "It enables the companion of the Intelligent Enterprise, guiding you through content discovery within SAP Ecosystem, " + "and giving a transparent role-based access to the relevant processes from everywhere. " + "This is the one assistant experience, a unified and delightful user experience across SAP’s solution portfolio." + ), + metadata=[VectorKeyValueListPair(key="index", value=["2"])], + ), + ], + ) + + request = DocumentsUpdateRequest(documents=[updated_doc]) + response = self.client.update_documents(self.__class__.collection_id, request) + + self.assertTrue(response.documents) + updated_metadata = response.documents[0].metadata[0] + self.assertEqual(updated_metadata.key, "url") + self.assertEqual(updated_metadata.value, ["http://hello1.com"]) + + # Follow-up check — ensure data is persisted via GET + fetched_doc = self.client.get_document_by_id(self.__class__.collection_id, self.__class__.document_id) + + self.assertEqual(fetched_doc.id, self.__class__.document_id) + fetched_metadata = {m.key: m.value for m in fetched_doc.metadata} + self.assertIn("url", fetched_metadata) + self.assertEqual(fetched_metadata["url"], ["http://hello1.com"]) + + first_chunk = fetched_doc.chunks[0] + second_chunk = fetched_doc.chunks[1] + + self.assertIn("not the AI copilot", first_chunk.content) + self.assertIn("It enables the companion of the Intelligent Enterprise", second_chunk.content) + + # --- Search --- + + # Perform a semantic vector search and verify results and data consistency. + self.assertTrue(self.__class__.collection_id) + + request = TextSearchRequest( + query="is Joule an AI Copilot?", + filters=[ + VectorSearchFilter( + id="string", + collectionIds=[self.__class__.collection_id], + configuration=VectorSearchConfiguration(), + collectionMetadata=[], + documentMetadata=[ + VectorSearchDocumentKeyValueListPair( + key="url", + value=["http://hello1.com"], + selectMode=["ignoreIfKeyAbsent"], + ) + ], + chunkMetadata=[], + ) + ], + ) + + response = self.client.search(request) + + self.assertTrue(response.results) + filter_result = response.results[0] + self.assertEqual(filter_result.filterId, "string") + self.assertTrue(filter_result.results) + + collection_result = filter_result.results[0] + self.assertEqual(collection_result.title, self.collection_title) + self.assertTrue(collection_result.metadata) + self.assertTrue(collection_result.documents) + + document_result = collection_result.documents[0] + metadata_dict = {m.key: m.value for m in document_result.metadata} + self.assertIn("url", metadata_dict) + self.assertEqual(metadata_dict["url"], ["http://hello1.com"]) + + self.assertTrue(document_result.chunks) + self.assertTrue(any("Joule" in chunk.content for chunk in document_result.chunks)) + + for chunk in document_result.chunks: + self.assertIsInstance(chunk.id, str) + self.assertIsInstance(chunk.content, str) + self.assertTrue(any(m.key == "index" for m in chunk.metadata)) + + # --- Cleanup --- + + # Delete a document (204 No Content). + self.assertTrue(self.__class__.collection_id) + self.assertTrue(self.__class__.document_id) + response = self.client.delete_document(self.__class__.collection_id, self.__class__.document_id) + self.assertEqual(response.status_code, requests.status_codes.codes.NO_CONTENT) + + # Delete a collection (204 No Content). + self.assertTrue(self.__class__.collection_id) + response = self.client.delete_collection(self.__class__.collection_id) + self.assertEqual(response.status_code, requests.status_codes.codes.NO_CONTENT) + + # Verify deletion status after deletion. + self.assertTrue(self.__class__.collection_id) + response = self.client.get_collection_deletion_status(self.__class__.collection_id) + self.assertIn(response.status, ["DELETED", "PENDING"]) \ No newline at end of file diff --git a/packages/gen/integration_tests/evaluations/__init__.py b/packages/gen/integration_tests/evaluations/__init__.py new file mode 100644 index 0000000..4df8981 --- /dev/null +++ b/packages/gen/integration_tests/evaluations/__init__.py @@ -0,0 +1 @@ +# Integration tests for evaluations module diff --git a/packages/gen/integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv b/packages/gen/integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv new file mode 100644 index 0000000..5005695 --- /dev/null +++ b/packages/gen/integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv @@ -0,0 +1,2 @@ +topic,sentiment,reference +how does rivatigmine and otc sleep medicine interact,Interaction,"tell your doctor and pharmacist what prescription and nonprescription medications, vitamins, nutritional supplements, and herbal products you are taking or plan to take. Be sure to mention any of the following: antihistamines; aspirin and other nonsteroidal anti-inflammatory medications (NSAIDs) such as ibuprofen (Advil, Motrin) and naproxen (Aleve, Naprosyn); bethanechol (Duvoid, Urecholine); ipratropium (Atrovent, in Combivent, DuoNeb); and medications for Alzheimer's disease, glaucoma, irritable bowel disease, motion sickness, ulcers, or urinary problems. Your doctor may need to change the doses of your medications or monitor you carefully for side effects." diff --git a/packages/gen/integration_tests/evaluations/test_base.py b/packages/gen/integration_tests/evaluations/test_base.py new file mode 100644 index 0000000..852d474 --- /dev/null +++ b/packages/gen/integration_tests/evaluations/test_base.py @@ -0,0 +1,74 @@ +""" +Base test class for evaluation integration tests. +Provides common setup and utilities for evaluation client tests. +""" +import unittest +from gen_ai_hub.evaluations.client import EvaluationClient +import os + + +class EvaluationClientTestBase(unittest.TestCase): + """Base class for evaluation client integration tests.""" + + @classmethod + def setUpClass(cls): + """Set up the test class with credentials.""" + # Hardcoded credentials (will be moved to environment variables later) + cls.base_url = os.getenv("AICORE_BASE_URL") + cls.auth_url = os.getenv("AICORE_AUTH_URL") + cls.client_id = os.getenv("AICORE_CLIENT_ID") + cls.client_secret = os.getenv("AICORE_CLIENT_SECRET") + cls.resource_group = "default" + cls.aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") + cls.aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") + cls.input_object_store_secret_name = "sdk-data" + + def setUp(self): + """Set up each test with a fresh evaluation client instance and object store secrets.""" + self.client = EvaluationClient( + base_url=self.base_url, + auth_url=self.auth_url, + client_id=self.client_id, + client_secret=self.client_secret, + resource_group=self.resource_group, + aws_access_key_id=self.aws_access_key_id, + aws_secret_access_key=self.aws_secret_access_key, + input_object_store_secret_name=self.input_object_store_secret_name, + ) + + # Setup S3 credentials for object store secrets + AWS_S3_ENDPOINT = "s3-eu-central-1.amazonaws.com" + AWS_BUCKET_ID = "hcp-e597ff51-40f5-42c9-a75a-744281742e61" + AWS_REGION = "eu-central-1" + + # default secret is needed to store output artifacts that the evaluation job creates after it is completed + default_secret_creds = { + "data": {}, + "type": "S3", + "pathPrefix": "sdkOutputFiles", + "endpoint": AWS_S3_ENDPOINT, + "bucket": AWS_BUCKET_ID, + "region": AWS_REGION, + "usehttps": "1", + } + + # input secret is used to load input artifacts required by the evaluation job. + # This is optional as these files can be loaded via default secret path as well. + input_secret_creds = { + "data": {}, + "name": "sdk-data", + "type": "S3", + "pathPrefix": "sdk_input_files/data", + "endpoint": AWS_S3_ENDPOINT, + "bucket": AWS_BUCKET_ID, + "region": AWS_REGION, + "usehttps": "1", + } + + # Creation of object store secrets and creates orchestration deployment url if not passed via initialization. + response = self.client.setup( + default_secret_body=default_secret_creds, + input_secret_body=input_secret_creds, + replace_existing=True + ) + self.client.orchestration_url = response.get("orchestration_url") diff --git a/packages/gen/integration_tests/evaluations/test_multiple_execution_flow.py b/packages/gen/integration_tests/evaluations/test_multiple_execution_flow.py new file mode 100755 index 0000000..4c915bf --- /dev/null +++ b/packages/gen/integration_tests/evaluations/test_multiple_execution_flow.py @@ -0,0 +1,332 @@ +""" +Integration tests for multiple execution flow in evaluations. +Tests evaluation with multiple EvaluationConfig objects - one with prompt template, +one with orchestration registry reference and custom metric. +""" +import requests +import unittest +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from integration_tests.evaluations.test_base import EvaluationClientTestBase + + +def get_auth_token(auth_url, client_id, client_secret): + """Get authentication token for API requests.""" + payload = { + 'grant_type': 'client_credentials', + 'client_id': client_id, + 'client_secret': client_secret, + } + response = requests.post(auth_url, data=payload) + response.raise_for_status() + response_data = response.json() + if 'access_token' in response_data: + return response_data['access_token'] + else: + raise Exception(f"Failed to get token: {response_data}") + +def create_prompt_template(base_url, headers): + """Create prompt template and return its ID.""" + api_url = f"{base_url}/lm/promptTemplates" + payload = { + "name": "sdktest-prompt-reg-eval", + "version": "1.0.0", + "scenario": "genai-evaluations", + "spec": { + "template": [ + { + "role": "user", + "content": "Provide a concise and informative response to the following consumer health question: {{?question}}" + } + ], + "defaults": {}, + "additionalFields": {} + } + } + print(f"Creating prompt template with payload: {payload}") + response = requests.post(api_url, headers=headers, json=payload) + response.raise_for_status() + result = response.json() + print(f"Prompt template created: {result.get('id')}") + return result['id'] + +def create_orchestration_registry_config(base_url, headers, model_name, model_version): + """Create orchestration registry configuration and return its ID.""" + # base_url already includes /v2, so we just append the path + api_url = f"{base_url}/registry/v2/orchestrationConfigs" + payload = { + "name": "genai-eval-test", + "version": "1.0.0", + "scenario": "genai-evaluations", + "spec": { + "modules": { + "prompt_templating": { + "model": { + "name": model_name, + "version": model_version + }, + "prompt": { + "template": [ + { + "role": "user", + "content": "Provide a concise and informative response to the following consumer health question: {{?question}}" + } + ], + "defaults": {} + } + } + } + } + } + response = requests.post(api_url, headers=headers, json=payload) + response.raise_for_status() + result = response.json() + print(f"Orchestration registry configuration created: {result.get('id')}") + return result['id'] + + +def create_custom_metric(base_url, headers, model_name, model_version): + """Create custom metric and return its ID.""" + # base_url already includes /v2, so we just append the path + api_url = f"{base_url}/lm/evaluationMetrics" + payload = { + "name": "eval-test-metric", + "scenario": "genai-evaluations-test", + "version": "0.0.1", + "evaluationMethod": "llm-as-a-judge", + "managedBy": "imperative", + "systemPredefined": "false", + "metricType": "evaluation", + "spec": { + "outputType": "numerical", + "promptType": "structured", + "configuration": { + "modelConfiguration": { + "name": "gpt-5", + "version": "2025-08-07", + "parameters": [ + { + "key": "max_tokens", + "value": "10000" + } + ] + }, + "promptConfiguration": { + "definition": "You are an expert evaluator. Your task is to evaluate the quality of the responses generated by AI models. We will provide you with a reference and an AI-generated response. You should first read the user input carefully for analyzing the task, and then evaluate the quality of the responses based on the criteria provided in the Evaluation section below. You will assign the response a rating following the Rating Rubric and Evaluation Steps. Give step-by-step explanations for your rating, and only choose ratings from the Rating Rubric.\n\n## Metric Definition\nYou are an INFORMATION OVERLAP classifier providing the overlap of information between a response and reference.\n\n## Criteria\nGroundedness: The of information between a response generated by AI models and provided reference.\n\n## Rating Rubric\n5: (Fully grounded). The response and the reference are fully overlapped.\n4: (Mostly grounded). The response and the reference are mostly overlapped.\n3: (Somewhat grounded). The response and the reference are somewhat overlapped.\n2: (Poorly grounded). The response and the reference are slightly overlapped.\n1: (Not grounded). There is no overlap between the response and the reference.\n\n## Evaluation Steps\nSTEP 1: Assess the response in aspects of Groundedness. Identify any information in the response and provide assessment according to the Criteria.\nSTEP 2: Score based on the rating rubric. Give a brief rationale to explain your evaluation considering Groundedness.\n\nReference: {{?reference}}\nResponse: {{?aicore_llm_completion}}\n\nBegin your evaluation by providing a short explanation. Be as unbiased as possible. After providing your explanation, please rate the response according to the rubric and outputs STRICTLY following this JSON format:\n\n{ \"explanation\": string, \"rating\": integer }\n\nOutput:\n", + "evaluationTask": "You are an expert evaluator. Your task is to evaluate the quality of the responses generated by AI models. We will provide you with a reference and an AI-generated response. You should first read the user input carefully for analyzing the task, and then evaluate the quality of the responses based on the criteria provided in the Evaluation section below. You will assign the response a rating following the Rating Rubric and Evaluation Steps. Give step-by-step explanations for your rating, and only choose ratings from the Rating Rubric.\n\n## Metric Definition\nYou are an INFORMATION OVERLAP classifier providing the overlap of information between a response and reference.\n\n## Criteria\nGroundedness: The of information between a response generated by AI models and provided reference.\n\n## Rating Rubric\n5: (Fully grounded). The response and the reference are fully overlapped.\n4: (Mostly grounded). The response and the reference are mostly overlapped.\n3: (Somewhat grounded). The response and the reference are somewhat overlapped.\n2: (Poorly grounded). The response and the reference are slightly overlapped.\n1: (Not grounded). There is no overlap between the response and the reference.\n\n## Evaluation Steps\nSTEP 1: Assess the response in aspects of Groundedness. Identify any information in the response and provide assessment according to the Criteria.\nSTEP 2: Score based on the rating rubric. Give a brief rationale to explain your evaluation considering Groundedness.\n\nReference: {{?reference}}\nResponse: {{?aicore_llm_completion}}\n\nBegin your evaluation by providing a short explanation. Be as unbiased as possible. After providing your explanation, please rate the response according to the rubric and outputs STRICTLY following this JSON format:\n\n{ \"explanation\": string, \"rating\": integer }\n\nOutput:\n", + "criteria": "You should strictly follow the instruction given to you. Please act as an impartial judge and evaluate the quality of the responses based on the prompt and following criteria:", + "ratingRubric": [ + { + "rating": 3, + "rule": "Response is completely factual with no unsupported claims" + }, + { + "rating": 2, + "rule": "Response has minor inaccuracies but no major contradictions" + }, + { + "rating": 1, + "rule": "Response contains significant factual errors or hallucinations" + } + ] + } + } + } + } + response = requests.post(api_url, headers=headers, json=payload) + response.raise_for_status() + result = response.json() + print(f"Custom metric created: {result.get('id')}") + return result['id'] + + +def delete_orchestration_registry_config(base_url, headers, config_id): + """Delete orchestration registry configuration.""" + if not config_id: + return + try: + api_url = f"{base_url}/registry/v2/orchestrationConfigs/{config_id}" + response = requests.delete(api_url, headers=headers) + if response.status_code < 300: + print(f"Orchestration registry configuration deleted: {config_id}") + else: + print(f"Warning: Failed to delete orchestration registry {config_id}: {response.status_code} - {response.text}") + except Exception as e: + print(f"Warning: Error deleting orchestration registry {config_id}: {e}") + +def delete_prompt_template(base_url, headers, template_id): + """Delete prompt template.""" + if not template_id: + return + try: + api_url = f"{base_url}/lm/promptTemplates/{template_id}" + response = requests.delete(api_url, headers=headers) + if response.status_code < 300: + print(f"Prompt template deleted: {template_id}") + else: + print(f"Warning: Failed to delete prompt template {template_id}: {response.status_code} - {response.text}") + except Exception as e: + print(f"Warning: Error deleting prompt template {template_id}: {e}") + +def delete_custom_metric(base_url, headers, metric_id): + """Delete custom metric.""" + if not metric_id: + return + try: + api_url = f"{base_url}/lm/evaluationMetrics/{metric_id}" + response = requests.delete(api_url, headers=headers) + if response.status_code < 300: + print(f"Custom metric deleted: {metric_id}") + else: + print(f"Warning: Failed to delete custom metric {metric_id}: {response.status_code} - {response.text}") + except Exception as e: + print(f"Warning: Error deleting custom metric {metric_id}: {e}") + + +class TestMultipleExecutionFlow(EvaluationClientTestBase): + """Test multiple evaluation execution flow.""" + + @classmethod + def setUpClass(cls): + """Set up class-level resources: orchestration registry and custom metric.""" + super().setUpClass() + + # Initialize IDs to None in case creation fails + cls.orchestration_registry_id = None + cls.custom_metric_id = None + cls.prompt_template_id = None + try: + # Get authentication token + token = get_auth_token(cls.auth_url, cls.client_id, cls.client_secret) + cls._headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + } + + # Model to use for orchestration registry and custom metric + model_name = "gpt-4o" + model_version = "latest" + + # Create orchestration registry configuration + cls.orchestration_registry_id = create_orchestration_registry_config( + cls.base_url, cls._headers, model_name, model_version + ) + + # Create prompt template + cls.prompt_template_id = create_prompt_template( + cls.base_url, cls._headers + ) + + # Create custom metric + cls.custom_metric_id = create_custom_metric( + cls.base_url, cls._headers, model_name, model_version + ) + except Exception as e: + print(f"Warning: Error during setUpClass: {e}") + # IDs remain None, tearDownClass will handle cleanup if needed + + @classmethod + def tearDownClass(cls): + """Clean up class-level resources: delete orchestration registry and custom metric.""" + # Refresh token for cleanup in case it expired + try: + token = get_auth_token(cls.auth_url, cls.client_id, cls.client_secret) + headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + } + except Exception as e: + print(f"Warning: Could not get token for cleanup: {e}") + headers = getattr(cls, '_headers', None) + if not headers: + print("Warning: No headers available for cleanup") + return + + # Delete orchestration registry configuration + if hasattr(cls, 'orchestration_registry_id') and cls.orchestration_registry_id: + delete_orchestration_registry_config( + cls.base_url, headers, cls.orchestration_registry_id + ) + + # Delete prompt template + if hasattr(cls, 'prompt_template_id') and cls.prompt_template_id: + delete_prompt_template( + cls.base_url, headers, cls.prompt_template_id + ) + + # Delete custom metric + if hasattr(cls, 'custom_metric_id') and cls.custom_metric_id: + delete_custom_metric( + cls.base_url, headers, cls.custom_metric_id + ) + + super().tearDownClass() + + def test_evaluate_with_prompt_template_and_orchestration_registry(self): + """Test evaluation with multiple configs: one with prompt template, one with orchestration registry reference and custom metric.""" + evaluation_configs = [ + # First config: Using prompt template + EvaluationConfig( + llm=LLM(name="gpt-4o", version="latest"), + template=TemplateRef(template_ref=TemplateRefByID(id=self.prompt_template_id)), + template_variable_mapping={"question": "topic"}, + dataset_config=Dataset("integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv"), + metrics=[ + MetricConfig( + reference=MetricRef(id="3ea07c1f-5b10-4b12-bf46-6d429faf8010"), + variable_mapping={"reference": "ground_truth"}, + ), + ], + ), + # Second config: Using orchestration registry reference with custom metric + EvaluationConfig( + orchestration_registry_reference=self.orchestration_registry_id, + template_variable_mapping={"question": "topic"}, + dataset_config=Dataset("integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv"), + metrics=[ + MetricConfig( + reference=MetricRef(id=self.custom_metric_id), + # variable_mapping={ + # "reference": "ground_truth", + # }, + ), + ], + ), + ] + + evaluation_runs = self.client.evaluate(evaluation_configs) + + self.assertIsNotNone(evaluation_runs) + self.assertEqual(len(evaluation_runs), 2) + + # Wait for all runs to complete and verify results + for i, run in enumerate(evaluation_runs): + run.wait_for_completion() + results = run.results() + metrics = results.metrics() + + expected_columns = { + "submission_id", + "run_id", + "repetition_count", + "metric", + "aggregating_value", + "metric_result", + "error", + } + + self.assertTrue( + expected_columns.issubset(metrics.columns), + f"Run {i+1}: Missing columns: {expected_columns - set(metrics.columns)}" + ) + self.assertTrue( + metrics["error"].isna().all(), + f"Run {i+1}: Some metric rows contain errors:\n{metrics[metrics['error'].notna()]}" + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/integration_tests/evaluations/test_single_execution_flow.py b/packages/gen/integration_tests/evaluations/test_single_execution_flow.py new file mode 100644 index 0000000..69dd40d --- /dev/null +++ b/packages/gen/integration_tests/evaluations/test_single_execution_flow.py @@ -0,0 +1,73 @@ +""" +Integration tests for single execution flow in evaluations. +Tests evaluation with a single EvaluationConfig. +""" +import unittest +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.prompt_registry.models.prompt_template import ( + PromptTemplateSpec, + PromptTemplate, +) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from integration_tests.evaluations.test_base import EvaluationClientTestBase + + +class TestSingleExecutionFlow(EvaluationClientTestBase): + """Test single evaluation execution flow.""" + + def test_evaluate_with_llm_and_template_spec(self): + """Test evaluation with LLM and PromptTemplateSpec.""" + evaluation_config = EvaluationConfig( + llm=LLM(name="gpt-4o", version="latest"), + template=PromptTemplateSpec( + template=[ + PromptTemplate( + role="user", + content="Provide a concise and informative response to the following consumer health question: {{?question}}" + ) + ] + ), + template_variable_mapping={"question": "topic"}, + dataset_config=Dataset("integration_tests/evaluations/eval-data/testdata/medicalqna_dataset.csv"), + metrics=[ + MetricConfig( + reference=MetricRef(id="3ea07c1f-5b10-4b12-bf46-6d429faf8010"), + variable_mapping={"reference": "ground_truth"}, + ), + ], + ) + evaluation_runs = self.client.evaluate(evaluation_config) + + self.assertIsNotNone(evaluation_runs) + self.assertEqual(len(evaluation_runs), 1) + + run = evaluation_runs[0] + run.wait_for_completion() + + results = run.results() + metrics = results.metrics() + + expected_columns = { + "submission_id", + "run_id", + "repetition_count", + "metric", + "aggregating_value", + "metric_result", + "error", + } + + self.assertTrue( + expected_columns.issubset(metrics.columns), + f"Missing columns: {expected_columns - set(metrics.columns)}" + ) + self.assertTrue( + metrics["error"].isna().all(), + f"Some metric rows contain errors:\n{metrics[metrics['error'].notna()]}" + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/integration_tests/gen_ai_hub_proxy/__init__.py b/packages/gen/integration_tests/gen_ai_hub_proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py b/packages/gen/integration_tests/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py new file mode 100644 index 0000000..f2eac10 --- /dev/null +++ b/packages/gen/integration_tests/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import importlib +import os +import unittest +from unittest.mock import patch + +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubProxyClient, Deployment +from integration_tests.constants import OPENAI_GPT_4O_MINI_TEST_MODEL +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + + +class TestProxyClient(TestCaseAICoreSetupMixin, unittest.TestCase): + + @staticmethod + def _are_deployments_same(d1: Deployment, d2: Deployment): + return (d1.url == d2.url and d1.deployment_id == d2.deployment_id and d1.model_name == d2.model_name + and d1.config_id == d2.config_id) + + def test_deployment_discovery(self): + proxy_client = self.proxy_client + self.assertGreaterEqual(len(proxy_client.deployments), len(self.aicore_deployments)) + self.assertIsInstance(proxy_client, GenAIHubProxyClient) + for kwarg in ('deployment_id', 'config_name', 'config_id', 'model_name', 'not_existing'): + kwargs = {kwarg: 'NOT_EXISTING'} + with self.assertRaises(ValueError): + proxy_client.select_deployment(**kwargs) + + aicore_test_deployment = self.aicore_deployments[OPENAI_GPT_4O_MINI_TEST_MODEL] + test_deployment = [dep for dep in proxy_client.deployments if dep.model_name == OPENAI_GPT_4O_MINI_TEST_MODEL][ + 0] + self.assertEqual(test_deployment.deployment_id, aicore_test_deployment.id) + + deployment = proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + self.assertTrue(self._are_deployments_same(deployment, test_deployment)) + deployment = proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL, model_version='latest') + self.assertTrue(self._are_deployments_same(deployment, test_deployment)) + for kwarg in ('deployment_id', 'config_id', 'model_name'): + value = getattr(test_deployment, kwarg) + deployment_other = proxy_client.select_deployment(**{kwarg: value}) + self.assertTrue(self._are_deployments_same(test_deployment, deployment_other)) + deployment_other = proxy_client.select_deployment(**{kwarg: value, 'executable_id': 'azure-openai'}) + self.assertTrue(self._are_deployments_same(test_deployment, deployment_other)) + with self.assertRaises(ValueError): + proxy_client.select_deployment(**{'executable_id': 'INVALID_EXECUTABLE_ID'}) + + +class TestCustomHeaders(unittest.TestCase): + """ + Test that custom AI_CLIENT_TYPE from environment variable is correctly applied + - i.e. propagated from ai-api-client and ai-core-sdk + """ + + def test_custom_ai_client_type_from_env(self): + test_client_type = 'Custom Integration Test Client' + original_value = os.environ.get('AI_CLIENT_TYPE') + + try: + os.environ['AI_CLIENT_TYPE'] = test_client_type + + # Setup: Reload the client module to pick up the new environment variable + from gen_ai_hub.proxy.gen_ai_hub_proxy import client + importlib.reload(client) + client.GenAIHubProxyClient.clear_cache() + + with proxy_version_context('gen-ai-hub'): + proxy_client = get_proxy_client() + headers = proxy_client.request_header + + self.assertIn('AI-Client-Type', headers) + self.assertEqual(test_client_type, headers['AI-Client-Type']) + + # Cleanup + finally: + if original_value is not None: + os.environ['AI_CLIENT_TYPE'] = original_value + else: + os.environ.pop('AI_CLIENT_TYPE', None) + + from gen_ai_hub.proxy.gen_ai_hub_proxy import client + importlib.reload(client) + client.GenAIHubProxyClient.clear_cache() + + def test_default_ai_client_type(self): + original_value = os.environ.get('AI_CLIENT_TYPE') + + try: + # Setup: Remove the environment variable if it exists + os.environ.pop('AI_CLIENT_TYPE', None) + from gen_ai_hub.proxy.gen_ai_hub_proxy import client + importlib.reload(client) + client.GenAIHubProxyClient.clear_cache() + + with proxy_version_context('gen-ai-hub'): + proxy_client = get_proxy_client() + + headers = proxy_client.request_header + + self.assertIn('AI-Client-Type', headers) + self.assertEqual('GenAI Hub SDK (Python)', headers['AI-Client-Type']) + + # Cleanup + finally: + if original_value is not None: + os.environ['AI_CLIENT_TYPE'] = original_value + else: + os.environ.pop('AI_CLIENT_TYPE', None) + + from gen_ai_hub.proxy.gen_ai_hub_proxy import client + importlib.reload(client) + client.GenAIHubProxyClient.clear_cache() diff --git a/packages/gen/integration_tests/langchain_/__init__.py b/packages/gen/integration_tests/langchain_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/langchain_/test_amazon.py b/packages/gen/integration_tests/langchain_/test_amazon.py new file mode 100644 index 0000000..8767efe --- /dev/null +++ b/packages/gen/integration_tests/langchain_/test_amazon.py @@ -0,0 +1,167 @@ +import unittest + +import pytest +from langchain_classic.chains import LLMChain +from langchain_classic.prompts.chat import ( + AIMessagePromptTemplate, + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate, +) +from langchain_core.messages import AIMessage, HumanMessage, AIMessageChunk +from parameterized import parameterized +from pydantic import BaseModel + +from gen_ai_hub.proxy.langchain.amazon import BedrockEmbeddings, ChatBedrock, ChatBedrockConverse +from integration_tests.constants import (AMAZON_TITAN_EMBEDDING_TEST_MODEL, CLAUDE_4_5_SONNET_TEST_MODEL, + AMAZON_NOVA_MICRO_TEST_MODEL, CLAUDE_4_5_HAIKU_TEST_MODEL) +from integration_tests.setup_aicore import TestCaseBedrockSetupMixin + +@pytest.mark.bedrock +class TestAmazonLLM(TestCaseBedrockSetupMixin, unittest.TestCase): + """ + Titan models were retired and replaced by (multimodal) nova models. + Documentation on nova models: https://docs.aws.amazon.com/nova/latest/userguide/what-is-nova.html + https://docs.aws.amazon.com/bedrock/latest/userguide/inference-methods.html + """ + + def test_invoke(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + chat_model = ChatBedrock( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + ) + template = "You are a helpful assistant that translates english to pirate." + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template("Hi") + example_ai = AIMessagePromptTemplate.from_template("Ahoy!") + human_template = "{text}" + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke("I love programming") + self.assertIsInstance(response["text"], str) + + def test_converse(self, model=AMAZON_NOVA_MICRO_TEST_MODEL): + with self.subTest(model=model): + chat_model = ChatBedrockConverse( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + ) + template = "You are a helpful assistant that translates english to pirate." + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template("Hi") + example_ai = AIMessagePromptTemplate.from_template("Ahoy!") + human_template = "{text}" + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke("I love programming") + self.assertIsInstance(response["text"], str) + + + + @parameterized.expand( + [ + CLAUDE_4_5_SONNET_TEST_MODEL, + CLAUDE_4_5_HAIKU_TEST_MODEL, + ] + ) + def test_stream(self, model= CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + chat_model = ChatBedrock( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + streaming=True + ) + + chunks = [chunk for chunk in chat_model.stream([HumanMessage(content="Why is the sky blue?")])] + self.assertTrue(all(isinstance(chunk, AIMessageChunk) for chunk in chunks)) + if "anthropic" in model: # Token by token streaming is only supported for anthropic models + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + def test_embed_query(self): + embedding_model = BedrockEmbeddings( + model_name=AMAZON_TITAN_EMBEDDING_TEST_MODEL, proxy_client=self.proxy_client + ) + response = embedding_model.embed_query("Your text string goes here") + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + + def test_structured_output(self): + + class Mountain(BaseModel): + name: str + country: str + height_in_meters: int + + chat_model = ChatBedrock( + model_name=CLAUDE_4_5_SONNET_TEST_MODEL, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + ) + + chat_model = chat_model.with_structured_output(Mountain) + mountain = chat_model.invoke( + [HumanMessage(content="What is the highest mountain in Japan?")] + ) + + self.assertIsInstance(mountain, Mountain) + self.assertIn("Fuji", mountain.name) + self.assertEqual(mountain.country, "Japan") + self.assertAlmostEqual(mountain.height_in_meters, 3776, delta=100) + + +@pytest.mark.bedrock +class TestAmazonLLMAsync(TestCaseBedrockSetupMixin, unittest.IsolatedAsyncioTestCase): + + async def test_chat_model(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + chat_model = ChatBedrock( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + ) + response = await chat_model.ainvoke( + [HumanMessage(content="Write me a song about sparkling water.")] + ) + self.assertIsInstance(response, AIMessage) + + async def test_chat_converse_model(self, model=AMAZON_NOVA_MICRO_TEST_MODEL): + with self.subTest(model=model): + chat_model = ChatBedrockConverse( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + ) + response = await chat_model.ainvoke( + [HumanMessage(content="Write me a song about sparkling water.")] + ) + self.assertIsInstance(response, AIMessage) + + async def test_async_chat_streaming(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + from langchain_classic.schema import HumanMessage + chat_model = ChatBedrock( + model_name=model, + model_kwargs={"temperature": 0.0}, + proxy_client=self.proxy_client, + streaming=True + ) + chunks = [chunk async for chunk + in chat_model.astream([HumanMessage(content='Write me a song about sparkling water.')])] + self.assertTrue(all(isinstance(chunk, AIMessageChunk) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") diff --git a/packages/gen/integration_tests/langchain_/test_google_genai.py b/packages/gen/integration_tests/langchain_/test_google_genai.py new file mode 100644 index 0000000..ab42cfc --- /dev/null +++ b/packages/gen/integration_tests/langchain_/test_google_genai.py @@ -0,0 +1,170 @@ +import unittest +import uuid + +from langchain_classic.chains import LLMChain +from langchain_classic.prompts import PromptTemplate +from langchain_classic.prompts.chat import ( + ChatPromptTemplate, + MessagesPlaceholder, +) +from langchain_classic.schema import BaseChatMessageHistory, HumanMessage +from langchain_community.chat_message_histories.in_memory import ChatMessageHistory +from langchain_core.messages import AIMessage +from langchain_core.runnables.history import RunnableWithMessageHistory + +from gen_ai_hub.proxy.langchain import init_llm +from gen_ai_hub.proxy.langchain.google_genai import ChatGoogleGenerativeAI +from gen_ai_hub.proxy.langchain.google_genai import GoogleGenerativeAIEmbeddings +from integration_tests.constants import GOOGLE_EMBEDDING_TEST_MODEL, GEMINI_2_5_FLASH_LITE_TEST_MODEL +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + + +class TestGoogleGenerativeAI(TestCaseAICoreSetupMixin, unittest.TestCase): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.model_histories = {} + + def _get_model_history(self, session_id: str) -> BaseChatMessageHistory: + return self.model_histories[session_id] + + def _create_model_history(self) -> str: + session_id = str(uuid.uuid4()) + self.model_histories[session_id] = ChatMessageHistory() + return session_id + + def test_genai_invoke(self, model=GEMINI_2_5_FLASH_LITE_TEST_MODEL): + chat_model = ChatGoogleGenerativeAI( + proxy_model_name=model, + proxy_client=self.proxy_client, + temperature=0 + ) + response = chat_model.invoke("Write a ballad about LangChain") + self.assertIsInstance(response.content, str) + + def test_genai_embedding(self, model=GOOGLE_EMBEDDING_TEST_MODEL): + embedding_model = GoogleGenerativeAIEmbeddings( + proxy_model_name=model, + proxy_client=self.proxy_client, + ) + vector = embedding_model.embed_query("hello, world!") + self.assertIsInstance(vector, list) + self.assertTrue(all(isinstance(x, float) for x in vector)) + + def test_genai_embedding_model_with_version(self, model=GOOGLE_EMBEDDING_TEST_MODEL): + embedding_model = GoogleGenerativeAIEmbeddings( + proxy_model_name=f'{model}-001', + proxy_client=self.proxy_client, + ) + vector = embedding_model.embed_query("hello, world!") + self.assertIsInstance(vector, list) + self.assertTrue(all(isinstance(x, float) for x in vector)) + + def test_genai_stream(self, model=GEMINI_2_5_FLASH_LITE_TEST_MODEL): + chat_model = ChatGoogleGenerativeAI( + proxy_model_name=model, + proxy_client=self.proxy_client, + temperature=0 + ) + response = chat_model.stream( + "You are a story teller. Write a story about a magic backpack." + ) + chunks = [chunk for chunk in response] + self.assertTrue(all(isinstance(chunk.content, str) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + def test_genai_langchain_history(self): + chat_model = ChatGoogleGenerativeAI( + proxy_model_name=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + proxy_client=self.proxy_client, + temperature=0 + ) + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are the large language model {llmname}. Always say good bye in {topicprompt} fashion. Limit your response to 25 words max.", + ), + MessagesPlaceholder(variable_name="messages"), + ], + ) + chat_model = prompt | chat_model + + history_id = self._create_model_history() + config = {"configurable": {"session_id": history_id}} + + # first prompt + model_with_message_history = RunnableWithMessageHistory( + chat_model, + self._get_model_history, + input_messages_key="messages", + ) + response = model_with_message_history.invoke( + { + "messages": [HumanMessage(content="My name is Jack Sparrow.")], + "llmname": GEMINI_2_5_FLASH_LITE_TEST_MODEL, + "topicprompt": "pirate", + }, + config=config, + ) + self.assertIsInstance(response.content, str) + + # second prompt + response = model_with_message_history.invoke( + { + "messages": [HumanMessage(content="What is my name?")], + "llmname": GEMINI_2_5_FLASH_LITE_TEST_MODEL, + "topicprompt": "pirate", + }, + config=config, + ) + self.assertIsInstance(response.content, str) + + def test_chat_from_prompt_template(self): + chat_model = ChatGoogleGenerativeAI( + proxy_model_name=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + proxy_client=self.proxy_client, + temperature=0 + ) + tweet_prompt = PromptTemplate.from_template( + "You are a content creator. Write me a tweet about {topic}." + ) + chain = LLMChain(llm=chat_model, prompt=tweet_prompt, verbose=True) + response = chain.invoke("how bad could floods affect Heidelberg, Germany") + self.assertIsInstance(response["text"], str) + + +class TestAsyncGoogleGenerativeAI(TestCaseAICoreSetupMixin, unittest.IsolatedAsyncioTestCase): + + async def test_genai_ainvoke(self): + llm = init_llm( + model_name=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + proxy_client=self.proxy_client, + max_tokens=1000 + ) + response = await llm.ainvoke("Write a ballad about LangChain") + self.assertIsInstance(response, AIMessage) + + async def test_genai_chat_ainvoke(self): + chat_model = ChatGoogleGenerativeAI( + model_name=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + proxy_client=self.proxy_client, + max_tokens=1000 + ) + response = await chat_model.ainvoke("Write a ballad about LangChain") + print(response) + self.assertIsInstance(response, AIMessage) + + async def test_genai_astream(self): + chat_model = ChatGoogleGenerativeAI( + proxy_model_name=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + proxy_client=self.proxy_client, + temperature=0 + ) + content = "You are a story teller. Write a story about a magic backpack." + chunks = [chunk async for chunk in chat_model.astream(content)] + self.assertTrue(all(isinstance(chunk.content, str) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/integration_tests/langchain_/test_model_init.py b/packages/gen/integration_tests/langchain_/test_model_init.py new file mode 100644 index 0000000..5ca0182 --- /dev/null +++ b/packages/gen/integration_tests/langchain_/test_model_init.py @@ -0,0 +1,78 @@ +import unittest +from typing import Callable + +from langchain_classic.chains import LLMChain +from langchain_classic.prompts import PromptTemplate + +from gen_ai_hub.proxy.langchain.init_models import ModelType, init_embedding_model, init_llm, _get_init_func +from integration_tests.constants import (OPENAI_GPT_4O_MINI_TEST_MODEL, + AMAZON_TITAN_EMBEDDING_TEST_MODEL, OPENAI_EMBEDDING_TEST_MODEL, + GEMINI_2_5_FLASH_LITE_TEST_MODEL) +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + + +class TestInitModels(TestCaseAICoreSetupMixin, unittest.TestCase): + + def test_init_llm(self): + template = """Question: {question} + Answer: Let's think step by step.""" + prompt = PromptTemplate(template=template, input_variables=['question']) + question = 'What is a supernova?' + models=[GEMINI_2_5_FLASH_LITE_TEST_MODEL, OPENAI_GPT_4O_MINI_TEST_MODEL] # CLAUDE_3_7_SONNET_TEST_MODEL + for model in models: + try: + self.proxy_client.select_deployment(model_name=model) + except ValueError: + # skip if deployment is not available + continue + llm = init_llm(model, max_tokens=24, proxy_client=self.proxy_client) + llm_chain = LLMChain(prompt=prompt, llm=llm) + answer = llm_chain.invoke(question) + self.assertIsInstance(answer['text'], str) + + def _do_test_custom_models(self, models: list, model_type: ModelType, init_model: Callable): + question = 'What is a supernova?' + for model_name in models: + try: + self.proxy_client.select_deployment(model_name=model_name) + except ValueError: + # skip if deployment is not available + continue + + init_func = _get_init_func(model_name, model_type) + model = init_model(model_name, max_tokens=24, proxy_client=self.proxy_client, init_func=init_func) + if model_type == ModelType.EMBEDDINGS: + response = model.embed_query(question) + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + else: + answer = model.invoke(question) + self.assertTrue(len(str(answer)) > 0, msg=f"Model {model_name} failed to generate a response") + + def test_init_custom_model(self): + """ + Test custom models that are not in the catalog. + Therefor, we delete the model entry from the catalog and test the model initialization. + Only one model per model provider is tested. + """ + self._do_test_custom_models(models=[GEMINI_2_5_FLASH_LITE_TEST_MODEL, OPENAI_GPT_4O_MINI_TEST_MODEL], # CLAUDE_3_7_SONNET_TEST_MODEL + model_type=ModelType.LLM, + init_model=init_llm) + + # test custom embedding models + self._do_test_custom_models(models=[AMAZON_TITAN_EMBEDDING_TEST_MODEL, OPENAI_EMBEDDING_TEST_MODEL], + model_type=ModelType.EMBEDDINGS, + init_model=init_embedding_model) + + def test_init_embedding_model(self): + text = 'What is a supernova?' + for model in [AMAZON_TITAN_EMBEDDING_TEST_MODEL, OPENAI_EMBEDDING_TEST_MODEL]: + try: + self.proxy_client.select_deployment(model_name=model) + except ValueError: + # skip if deployment is not available + continue + emb = init_embedding_model(model, proxy_client=self.proxy_client) + response = emb.embed_query(text) + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) diff --git a/packages/gen/integration_tests/langchain_/test_openai.py b/packages/gen/integration_tests/langchain_/test_openai.py new file mode 100644 index 0000000..42b47fa --- /dev/null +++ b/packages/gen/integration_tests/langchain_/test_openai.py @@ -0,0 +1,156 @@ +import unittest + +from parameterized import parameterized +from pydantic import BaseModel + +from integration_tests.constants import (OPENAI_GPT_O4_MINI_TEST_MODEL, + OPENAI_GPT_4O_MINI_TEST_MODEL, OPENAI_GPT_5_TEST_MODEL_NANO, OPENAI_EMBEDDING_TEST_MODEL, + OPENAI_GPT_O3_MINI_TEST_MODEL, MISTRAL_TEST_MODEL, NVIDIA_EMBEDDING_TEST_MODEL, PERPLEXITY_TEST_MODEL, + COHERE_COMMAND_A_TEST_MODEL, PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL) +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + +try: + import openai + + no_openai = False +except ImportError: + no_openai = True +try: + from gen_ai_hub.proxy.langchain.openai import ChatOpenAI, OpenAIEmbeddings + from langchain_classic.chains import LLMChain + from langchain_classic.prompts.chat import ( + AIMessagePromptTemplate, + ChatPromptTemplate, + HumanMessagePromptTemplate, + ) + from langchain_classic.schema import AIMessage, HumanMessage + from langchain_core.messages.ai import AIMessageChunk + + no_langchain = False +except ImportError: + no_langchain = True + + +class Person(BaseModel): + """ + A simple Pydantic model to test structured outputs with LangChain. + """ + name: str + age: int + + +@unittest.skipIf(no_openai or no_langchain, 'langchain or openai not installed') +class TestOpenAILLM(TestCaseAICoreSetupMixin, unittest.TestCase): + + @parameterized.expand( + [ + OPENAI_GPT_O4_MINI_TEST_MODEL, + PERPLEXITY_TEST_MODEL, + PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL, + ] + ) + def test_chat_model(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + chat_model = ChatOpenAI(proxy_model_name=model, proxy_client=self.proxy_client, max_retries=10) + self.assertIsNotNone(chat_model.model_name) + + example_human = HumanMessagePromptTemplate.from_template('Hi') + example_ai = AIMessagePromptTemplate.from_template('Ahoy!') + human_template = '{text}' + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke('I love programming') + self.assertIsInstance(response['text'], str) + + def test_chat_model_streaming(self, model=OPENAI_GPT_5_TEST_MODEL_NANO): + from langchain_classic.schema import HumanMessage + chat = ChatOpenAI(proxy_client=self.proxy_client, + proxy_model_name=model, + streaming=True, + temperature=0, + max_retries=10) + chunks = [*chat.stream([HumanMessage(content='Write me a song about sparkling water.')])] + self.assertIsNotNone(chat.model_name) + self.assertTrue(all(isinstance(chunk, AIMessageChunk) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + def test_embedding_model(self, model=OPENAI_EMBEDDING_TEST_MODEL): + embedding_model = OpenAIEmbeddings(proxy_model_name=model, proxy_client=self.proxy_client) + self.assertIsNotNone(embedding_model.model) + response = embedding_model.embed_query('Your text string goes here') + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + + def test_nvidia_embedding_model(self): + embedding_model = OpenAIEmbeddings( + proxy_client=self.proxy_client, + proxy_model_name=NVIDIA_EMBEDDING_TEST_MODEL, + input_type='query' + ) + self.assertIsNotNone(embedding_model.model) + response = embedding_model.embed_query('Your text string goes here') + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + + + def test_structured_outputs(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + chat_model = ChatOpenAI(proxy_model_name=model, proxy_client=self.proxy_client, max_retries=10) + chat_model = chat_model.with_structured_output(method="json_schema", schema=Person, strict=True) + + message = HumanMessage(content="Tell me about a person named John who is 30") + response = chat_model.invoke([message]) + self.assertIsInstance(response, Person) + + def test_cohere_command_a_reasoning(self, model=COHERE_COMMAND_A_TEST_MODEL): + """Test Cohere Command-A reasoning model via OpenAI compatibility API.""" + chat_model = ChatOpenAI(proxy_model_name=model, proxy_client=self.proxy_client, temperature=0.5, max_tokens=100, + max_retries=10) + message = HumanMessage(content='Explain the concept of recursion in programming.') + response = chat_model.invoke([message]) + self.assertIsInstance(response, AIMessage) + self.assertIsNotNone(response.content) + + +@unittest.skipIf(no_openai, 'openai not installed') +class AsyncOpenAITests(TestCaseAICoreSetupMixin, unittest.IsolatedAsyncioTestCase): + + @parameterized.expand( + [ + OPENAI_GPT_O3_MINI_TEST_MODEL, + MISTRAL_TEST_MODEL, + PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL + ] + ) + async def test_chat_model(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + chat_model = ChatOpenAI(proxy_model_name=model, proxy_client=self.proxy_client, max_retries=10) + self.assertIsNotNone(chat_model.model_name) + response = await chat_model.ainvoke([HumanMessage(content='Write me a song about sparkling water.')]) + self.assertIsInstance(response, AIMessage) + + async def test_async_chat_streaming(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + from langchain_classic.schema import HumanMessage + chat = ChatOpenAI(proxy_client=self.proxy_client, + proxy_model_name=model, + streaming=True, + temperature=1, + max_retries=10) + chunks = [chunk async for chunk + in chat.astream([HumanMessage(content='Write me a song about sparkling water.')])] + self.assertTrue(all(isinstance(chunk, AIMessageChunk) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + async def test_async_structured_outputs(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + chat_model = ChatOpenAI(proxy_model_name=model, proxy_client=self.proxy_client, max_retries=10) + chat_model = chat_model.with_structured_output(method="json_schema", schema=Person, strict=True) + + message = HumanMessage(content="Tell me about a person named John who is 30") + response = await chat_model.ainvoke([message]) + self.assertIsInstance(response, Person) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/integration_tests/native_clients/__init__.py b/packages/gen/integration_tests/native_clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/native_clients/test_amazon.py b/packages/gen/integration_tests/native_clients/test_amazon.py new file mode 100644 index 0000000..60b44c1 --- /dev/null +++ b/packages/gen/integration_tests/native_clients/test_amazon.py @@ -0,0 +1,507 @@ +import json +import unittest + +import pytest +from parameterized import parameterized + +from gen_ai_hub.proxy.native.amazon.clients import AsyncSession +from gen_ai_hub.proxy.native.amazon.clients import Session +from integration_tests.constants import (AMAZON_NOVA_MICRO_TEST_MODEL, AMAZON_NOVA_PREMIER_TEST_MODEL, + AMAZON_TITAN_EMBEDDING_TEST_MODEL, CLAUDE_4_5_SONNET_TEST_MODEL, + CLAUDE_4_5_HAIKU_TEST_MODEL) +from integration_tests.setup_aicore import TestCaseBedrockSetupMixin + + +@pytest.mark.bedrock +class AmazonAITests(TestCaseBedrockSetupMixin, unittest.TestCase): + """ + Titan models were retired and replaced by (multimodal) nova models. + Documentation on nova models: https://docs.aws.amazon.com/nova/latest/userguide/what-is-nova.html] + https://docs.aws.amazon.com/bedrock/latest/userguide/inference-methods.html + """ + def test_client_discovery(self): + amazon_deployments = [ + deployment + for deployment in self.proxy_client.deployments + if any( + element in deployment.model_name + for element in [ + AMAZON_TITAN_EMBEDDING_TEST_MODEL, + AMAZON_NOVA_PREMIER_TEST_MODEL, + CLAUDE_4_5_SONNET_TEST_MODEL, + ] + ) + ] + self.assertGreater( + len(amazon_deployments), 0, "No amazon virtual deployments found" + ) + + def test_bedrock_invoke_model(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model) + + body = json.dumps( + { + "max_tokens": 512, + "messages": [ + { + "role": "user", + "content": "Describe the purpose of a 'hello world' program in one line.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "temperature": 0.0 + } + ) + + response = bedrock.invoke_model( + body=body, + ) + + response_body = json.loads(response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["content"][0]["text"], str) + + def test_bedrock_invoke_model_with_version(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model, model_version="latest") + + body = json.dumps( + { + "max_tokens": 512, + "messages": [ + { + "role": "user", + "content": "Describe the purpose of a 'hello world' program in one line.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "temperature": 0.0 + } + ) + + response = bedrock.invoke_model( + body=body, + ) + + response_body = json.loads(response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["content"][0]["text"], str) + + def test_bedrock_invoke_model_with_response_stream(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model) + + body = json.dumps( + { + "max_tokens": 512, + "messages": [ + { + "role": "user", + "content": "You are a story teller. Tell me a story about cats.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "temperature": 0.0 + } + ) + response = bedrock.invoke_model_with_response_stream( + body=body + ) + + number_of_chunks = 0 + for event in response["body"]: + chunk = json.loads(event["chunk"]["bytes"]) + if chunk["type"] == "content_block_delta": + self.assertIsInstance(chunk["delta"].get("text", ""), str) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + last_chunk = chunk + self.assertEqual(last_chunk["type"], "message_stop") + self.assertIn("amazon-bedrock-invocationMetrics", last_chunk) + metrics = last_chunk["amazon-bedrock-invocationMetrics"] + self.assertIsInstance(metrics, dict) + self.assertIn("inputTokenCount", metrics) + self.assertGreater(metrics["inputTokenCount"], 0) + self.assertIn("outputTokenCount", metrics) + self.assertGreater(metrics["outputTokenCount"], 0) + + @parameterized.expand( + [ + CLAUDE_4_5_SONNET_TEST_MODEL, + CLAUDE_4_5_HAIKU_TEST_MODEL, + AMAZON_NOVA_PREMIER_TEST_MODEL, + ] + ) + def test_amazon_bedrock_converse(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model) + conversation = [ + { + "role": "user", + "content": [ + { + "text": "Describe the purpose of a 'hello world' program in one line." + } + ], + } + ] + response = bedrock.converse( + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.0}, + ) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response["output"]["message"]["content"][0]["text"], str) + + def test_amazon_bedrock_converse_with_tool(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model) + conversation = [ + { + "role": "user", + "content": [ + { + "text": "Create a person named Jenny who is 24 years old." + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "create_person", + "description": "Create a person with name and age.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the person." + }, + "age": { + "type": "integer", + "description": "The age of the person." + } + }, + "required": ["name", "age"] + } + } + } + } + ] + } + + response = bedrock.converse( + messages=conversation, + toolConfig=tool_config, + inferenceConfig={"maxTokens": 512, "temperature": 0.0}, + ) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + + output_message = response["output"]["message"] + self.assertEqual(output_message["role"], "assistant") + + content = output_message["content"] + tool_use_block = None + for c in content: + if "toolUse" in c: + tool_use_block = c["toolUse"] + break + + self.assertIsNotNone(tool_use_block, "No tool use block in response") + self.assertEqual(tool_use_block["name"], "create_person") + + tool_input = tool_use_block["input"] + self.assertIsInstance(tool_input, dict) + self.assertEqual(tool_input.get("name"), "Jenny") + self.assertEqual(tool_input.get("age"), 24) + + @parameterized.expand( + [ + AMAZON_NOVA_MICRO_TEST_MODEL, + CLAUDE_4_5_SONNET_TEST_MODEL, + CLAUDE_4_5_HAIKU_TEST_MODEL, + ] + ) + def test_amazon_bedrock_converse_stream(self, model=AMAZON_NOVA_MICRO_TEST_MODEL): + with self.subTest(model=model): + bedrock = Session().client(model_name=model) + conversation = [ + { + "role": "user", + "content": [ + { + "text": "List all planets in our solar system and give some details about the density, " + "temperature and gravity of each planet." + } + ], + } + ] + + response = bedrock.converse_stream( + messages=conversation, + inferenceConfig={"maxTokens": 4090, "temperature": 0.0} + ) + + stream = response['stream'] + response_metadata = response['ResponseMetadata'] + self.assertIsInstance(response_metadata, dict) + self.assertIn('HTTPHeaders', response_metadata) + self.assertIsInstance(response_metadata['HTTPHeaders'], dict) + headers = response_metadata['HTTPHeaders'] + self.assertIn('content-type', headers) + self.assertEqual(headers['content-type'], 'application/vnd.amazon.eventstream') + number_of_chunks = 0 + last_three_chunks = [] + for chunk in stream: + number_of_chunks += 1 + if number_of_chunks == 1: + self.assertIn('messageStart', chunk) + if 'contentBlockDelta' in chunk: + delta = chunk['contentBlockDelta'] + self.assertIsInstance(delta, dict) + if 'text' in delta: + text = delta['text'] + self.assertIsInstance(text, str) + last_three_chunks.append(chunk) + if len(last_three_chunks) > 3: + last_three_chunks.pop(0) + content_block_stop = last_three_chunks[0] + self.assertIn('contentBlockStop', content_block_stop) + message_stop = last_three_chunks[1] + self.assertIn('messageStop', message_stop) + metadata = last_three_chunks[2] + self.assertIn('metadata', metadata) + metadata = metadata['metadata'] + self.assertIsInstance(metadata, dict) + self.assertIn('usage', metadata) + usage = metadata['usage'] + self.assertIsInstance(usage, dict) + self.assertIn('inputTokens', usage) + self.assertIn('outputTokens', usage) + self.assertIn('totalTokens', usage) + + + def test_amazon_titan_embedding(self): + bedrock = Session().client(model_name=AMAZON_TITAN_EMBEDDING_TEST_MODEL) + body = json.dumps( + { + "inputText": "Please recommend books with a theme similar to the movie 'Inception'.", + } + ) + response = bedrock.invoke_model( + body=body, + ) + response_body = json.loads(response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["embedding"], list) + self.assertTrue( + all(isinstance(item, float) for item in response_body["embedding"]) + ) + + +@pytest.mark.bedrock +class AsyncAmazonAITests(TestCaseBedrockSetupMixin, unittest.IsolatedAsyncioTestCase): + + async def test_async_bedrock_invoke_model(self, model=CLAUDE_4_5_SONNET_TEST_MODEL): + session = AsyncSession() + bedrock = await session.async_client(model_name=model) + body = json.dumps( + { + "messages": [ + { + "role": "user", + "content": "Describe the purpose of a 'hello world' program in one line.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 512, + "temperature": 0.0 + } + ) + + response = await bedrock.invoke_model( + body=body, + ) + response_body = json.loads(await response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["content"][0]["text"], str) + + async def test_async_bedrock_invoke_model_with_model_version(self): + session = AsyncSession() + bedrock = await session.async_client(model_name=CLAUDE_4_5_SONNET_TEST_MODEL, model_version="latest") + body = json.dumps( + { + "messages": [ + { + "role": "user", + "content": "Describe the purpose of a 'hello world' program in one line.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 512, + "temperature": 0.0 + } + ) + + response = await bedrock.invoke_model( + body=body, + ) + response_body = json.loads(await response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["content"][0]["text"], str) + + async def test_async_bedrock_invoke_with_stream(self): + session = AsyncSession() + bedrock = await session.async_client(model_name=CLAUDE_4_5_SONNET_TEST_MODEL) + + body = json.dumps( + { + "max_tokens": 512, + "messages": [ + { + "role": "user", + "content": "You are a story teller. Tell me a story about cats.", + } + ], + "anthropic_version": "bedrock-2023-05-31", + "temperature": 0.0 + } + ) + + response = await bedrock.invoke_model_with_response_stream(body=body) + number_of_chunks = 0 + async for event in response["body"]: + chunk = json.loads(event["chunk"]["bytes"]) + if chunk["type"] == "content_block_delta": + self.assertIsInstance(chunk["delta"].get("text", ""), str) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + last_chunk = chunk + self.assertEqual(last_chunk["type"], "message_stop") + self.assertIn("amazon-bedrock-invocationMetrics", last_chunk) + metrics = last_chunk["amazon-bedrock-invocationMetrics"] + self.assertIsInstance(metrics, dict) + self.assertIn("inputTokenCount", metrics) + self.assertGreater(metrics["inputTokenCount"], 0) + self.assertIn("outputTokenCount", metrics) + self.assertGreater(metrics["outputTokenCount"], 0) + + @parameterized.expand( + [ + AMAZON_NOVA_PREMIER_TEST_MODEL, + CLAUDE_4_5_SONNET_TEST_MODEL + ] + ) + async def test_async_amazon_bedrock_converse(self, model): + session = AsyncSession() + bedrock = await session.async_client(model_name=model) + conversation = [ + { + "role": "user", + "content": [ + { + "text": "Describe the purpose of a 'hello world' program in one line." + } + ], + } + ] + + response = await bedrock.converse( + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.0}, + ) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response["output"]["message"]["content"][0]["text"], str) + + @parameterized.expand( + [ + AMAZON_NOVA_PREMIER_TEST_MODEL, + CLAUDE_4_5_SONNET_TEST_MODEL + ] + ) + async def test_async_amazon_bedrock_converse_stream(self, model): + session = AsyncSession() + bedrock = await session.async_client(model_name=model) + conversation = [ + { + "role": "user", + "content": [ + { + "text": "Describe the purpose of a 'hello world' program in one line." + } + ], + } + ] + + response = await bedrock.converse_stream( + messages=conversation, + inferenceConfig={"maxTokens": 512, "temperature": 0.0}, + ) + + stream = response['stream'] + response_metadata = response['ResponseMetadata'] + self.assertIsInstance(response_metadata, dict) + self.assertIn('HTTPHeaders', response_metadata) + self.assertIsInstance(response_metadata['HTTPHeaders'], dict) + headers = response_metadata['HTTPHeaders'] + self.assertIn('content-type', headers) + self.assertEqual(headers['content-type'], 'application/vnd.amazon.eventstream') + number_of_chunks = 0 + last_three_chunks = [] + async for chunk in stream: + number_of_chunks += 1 + if number_of_chunks == 1: + self.assertIn('messageStart', chunk) + if 'contentBlockDelta' in chunk: + delta = chunk['contentBlockDelta'] + self.assertIsInstance(delta, dict) + if 'text' in delta: + text = delta['text'] + self.assertIsInstance(text, str) + last_three_chunks.append(chunk) + if len(last_three_chunks) > 3: + last_three_chunks.pop(0) + content_block_stop = last_three_chunks[0] + self.assertIn('contentBlockStop', content_block_stop) + message_stop = last_three_chunks[1] + self.assertIn('messageStop', message_stop) + metadata = last_three_chunks[2] + self.assertIn('metadata', metadata) + metadata = metadata['metadata'] + self.assertIsInstance(metadata, dict) + self.assertIn('usage', metadata) + usage = metadata['usage'] + self.assertIsInstance(usage, dict) + self.assertIn('inputTokens', usage) + self.assertIn('outputTokens', usage) + self.assertIn('totalTokens', usage) + + async def test_async_amazon_titan_embedding(self): + session = AsyncSession() + bedrock = await session.async_client(model_name=AMAZON_TITAN_EMBEDDING_TEST_MODEL) + body = json.dumps( + { + "inputText": "Please recommend books with a theme similar to the movie 'Inception'.", + } + ) + response = await bedrock.invoke_model( + body=body, + ) + response_body = json.loads(await response.get("body").read()) + self.assertIsInstance(response, dict) + self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200) + self.assertIsInstance(response_body["embedding"], list) + self.assertTrue( + all(isinstance(item, float) for item in response_body["embedding"]) + ) diff --git a/packages/gen/integration_tests/native_clients/test_google_genai.py b/packages/gen/integration_tests/native_clients/test_google_genai.py new file mode 100644 index 0000000..5c4af7e --- /dev/null +++ b/packages/gen/integration_tests/native_clients/test_google_genai.py @@ -0,0 +1,122 @@ +import unittest + +from google.genai.types import GenerateContentResponse, GenerateContentConfig, Content, EmbedContentResponse, Part + +from gen_ai_hub.proxy.native.google_genai.clients import Client +from integration_tests.constants import GEMINI_2_5_FLASH_LITE_TEST_MODEL, GOOGLE_EMBEDDING_TEST_MODEL +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + + +def get_test_messages(text="Write a story about a magic backpack."): + if not text: + text = "Write a story about a magic backpack." + user_prompt_content = Content( + role="user", + parts=[ + Part(text=text), + ], + ) + return [user_prompt_content] + + +class GoogleGenAITests(TestCaseAICoreSetupMixin, unittest.TestCase): + + def test_client_discovery(self): + google_deployments = [ + deployment + for deployment in self.proxy_client.deployments + if any(element in deployment.model_name for element in ["gemini"]) + ] + self.assertGreater( + len(google_deployments), 0, "No google virtual deployments found" + ) + + def test_genai_chat(self): + client = Client(proxy_client=self.proxy_client) + chat_session = client.chats.create( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL + ) + model_response = chat_session.send_message("Hello.") + self.assertIsInstance(model_response, GenerateContentResponse) + model_response = chat_session.send_message( + "I am also fine. What are your plans for today?" + ) + self.assertIsInstance(model_response, GenerateContentResponse) + + def test_genai_generate_content(self): + client = Client(proxy_client=self.proxy_client) + response = client.models.generate_content( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + contents=get_test_messages(), + config=GenerateContentConfig(temperature=0), + ) + self.assertIsInstance(response, GenerateContentResponse) + + def test_genai_stream_generate_content(self): + client = Client(proxy_client=self.proxy_client) + response = client.models.generate_content_stream( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + contents=get_test_messages( + text="You are a story teller. Write a paragraph about a magic kingdom." + ), + config=GenerateContentConfig(temperature=0), + ) + chunks = [chunk for chunk in response] + self.assertTrue(all(isinstance(chunk.text, str) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + def test_genai_embedding(self): + client = Client(proxy_client=self.proxy_client) + response = client.models.embed_content( + model=GOOGLE_EMBEDDING_TEST_MODEL, + contents="What's the meaning of life?" + ) + self.assertIsInstance(response, EmbedContentResponse) + + def test_genai_embedding_model_name_with_version(self): + client = Client(proxy_client=self.proxy_client) + response = client.models.embed_content( + model=f"{GOOGLE_EMBEDDING_TEST_MODEL}-001", + contents="What's the meaning of life?" + ) + self.assertIsInstance(response, EmbedContentResponse) + +class AsyncGoogleGenAITests(TestCaseAICoreSetupMixin, unittest.IsolatedAsyncioTestCase): + + async def test_genai_generate_content_async(self): + async with Client(proxy_client=self.proxy_client).aio as aclient: + response = await aclient.models.generate_content( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + contents=get_test_messages(), + config=GenerateContentConfig(temperature=0), + ) + self.assertIsInstance(response, GenerateContentResponse) + + async def test_genai_chat_async(self): + async with Client(proxy_client=self.proxy_client).aio as aclient: + chat_session = aclient.chats.create( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL + ) + model_response = await chat_session.send_message("Hello.") + self.assertIsInstance(model_response, GenerateContentResponse) + model_response = await chat_session.send_message( + "What is your opinion about latest Gemini model?" + ) + self.assertIsInstance(model_response, GenerateContentResponse) + + async def test_genai_stream_generate_content_async(self): + async with Client(proxy_client=self.proxy_client).aio as aclient: + async_response_stream = await aclient.models.generate_content_stream( + model=GEMINI_2_5_FLASH_LITE_TEST_MODEL, + contents=get_test_messages( + text="You are a story teller. Write a paragraph about a magic kingdom." + ), + config=GenerateContentConfig(temperature=0), + ) + chunks = [chunk async for chunk in async_response_stream] + self.assertTrue(all(isinstance(chunk.text, str) for chunk in chunks)) + self.assertGreater(len(chunks), 1, "Only one chunk received - stream seems to be buffered.") + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/integration_tests/native_clients/test_openai.py b/packages/gen/integration_tests/native_clients/test_openai.py new file mode 100644 index 0000000..94fcbec --- /dev/null +++ b/packages/gen/integration_tests/native_clients/test_openai.py @@ -0,0 +1,430 @@ +import unittest +from parameterized import parameterized +from pydantic import BaseModel + +from integration_tests.constants import (MISTRAL_TEST_MODEL, OPENAI_EMBEDDING_TEST_MODEL, + OPENAI_GPT_5_MINI_TEST_MODEL, OPENAI_GPT_O4_MINI_TEST_MODEL, OPENAI_GPT_4O_MINI_TEST_MODEL, + OPENAI_GPT_O3_MINI_TEST_MODEL, OPENAI_GPT_5_TEST_MODEL_NANO, NVIDIA_EMBEDDING_TEST_MODEL, PERPLEXITY_TEST_MODEL, + COHERE_COMMAND_A_TEST_MODEL, OPENAI_GPT_5_TEST_MODEL, + PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL) + +try: + import openai + from gen_ai_hub.proxy.native.openai import AsyncOpenAI, OpenAI + from gen_ai_hub.proxy.native.openai.clients import ChatCompletions + from openai.types import Completion, CreateEmbeddingResponse + from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionUserMessageParam + from openai.types.responses import Response + + no_openai = False +except ImportError: + no_openai = True + +from integration_tests.setup_aicore import TestCaseAICoreSetupMixin + +class Person(BaseModel): + """ + A simple Pydantic model for testing structured outputs with OpenAI. + """ + name: str + age: int + + +@unittest.skipIf(no_openai, 'openai not installed') +class OpenAITests(TestCaseAICoreSetupMixin, unittest.TestCase): + + def test_client_discovery(self): + self.assertGreater(len(self.proxy_client.deployments), 0, 'No deployments found') + + def test_embedding(self, model=OPENAI_EMBEDDING_TEST_MODEL): + kwargs = {'input': 'Your text string goes here', 'model_name': model} + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).with_raw_response.embeddings.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_embedding_global(self, model=OPENAI_EMBEDDING_TEST_MODEL): + kwargs = {'input': 'Your text string goes here', 'model_name': model} + from gen_ai_hub.proxy.native.openai import embeddings + response = embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + + def test_nvidia_embedding(self, model=NVIDIA_EMBEDDING_TEST_MODEL): + kwargs = { + 'input': 'Your text string goes here', + 'model_name': model, + 'extra_body': {'input_type': 'query'} # NVIDIA embedding model requires input_type + } + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).with_raw_response.embeddings.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + @parameterized.expand( + [ + OPENAI_GPT_5_TEST_MODEL_NANO, + PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL + ] + ) + def test_chat_completion(self, model=OPENAI_GPT_5_TEST_MODEL_NANO): + messages = self._get_test_messages() + kwargs = dict(model_name=model, messages=messages, max_completion_tokens=1024, seed=42) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).with_raw_response.chat.completions.create( + **kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_gpt_version_models(self, config_name="gpt-5-nano-latest"): + messages = self._get_test_messages() + kwargs = dict(config_name=config_name, messages=messages, temperature=0, seed=42) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).with_raw_response.chat.completions.create( + **kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_gpt_model_version_in_params(self, model_name=OPENAI_GPT_5_TEST_MODEL_NANO, model_version="latest"): + messages = self._get_test_messages() + kwargs = dict(model_name=model_name, model_version=model_version, messages=messages, temperature=0, seed=42) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).with_raw_response.chat.completions.create( + **kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_chat_completion_global(self, model=OPENAI_GPT_5_TEST_MODEL_NANO): + from gen_ai_hub.proxy.native.openai import chat + messages = self._get_test_messages() + kwargs = dict(model_name=model, messages=messages, temperature=0, seed=42) + response = chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + + def test_completion(self, model=MISTRAL_TEST_MODEL): + prompt = ["This is a test"] + kwargs = dict(model_name=model, prompt=prompt, temperature=0, seed=42) + response = OpenAI(proxy_client=self.proxy_client).completions.create(**kwargs) + self.assertIsInstance(response, Completion) + response = OpenAI(proxy_client=self.proxy_client).with_raw_response.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_completion_global(self, model=MISTRAL_TEST_MODEL): + from gen_ai_hub.proxy.native.openai import completions + prompt = ["This is a test"] + kwargs = dict(model_name=model, prompt=prompt, temperature=0, seed=42) + response = completions.create(**kwargs) + self.assertIsInstance(response, Completion) + + def test_structured_outputs(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + response = client.chat.completions.parse( + model=model, + messages=[ChatCompletionUserMessageParam(role="user", content="Tell me about John Doe aged 30.")], + response_format=Person + ) + person = response.choices[0].message.parsed # Fully typed Person object + self.assertIsInstance(person, Person) + + @parameterized.expand( + [ + OPENAI_GPT_5_MINI_TEST_MODEL, + OPENAI_GPT_O4_MINI_TEST_MODEL, + OPENAI_GPT_O3_MINI_TEST_MODEL, + ] + ) + def test_chat_completion_streaming(self, model=OPENAI_GPT_O3_MINI_TEST_MODEL): + messages = self._get_test_messages() + kwargs = {'model_name': model, 'messages': messages, 'stream': True, 'temperature': 0, 'seed': 42} + generator = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(generator, openai.Stream) + chunks = [value for value in generator] + self.assertTrue(all(isinstance(chunk, ChatCompletionChunk) for chunk in chunks)) + self.assertGreater(len(chunks), 1, 'Only one chunk received - stream seems to be buffered.') + + def test_structured_outputs_with_streaming(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + """ + streaming structured outputs requires the full object to be returned. + For more information, see https://www.github.com/openai/openai-python#with_streaming_response + """ + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + with client.chat.completions.with_streaming_response.parse( + model=model, + messages=[ChatCompletionUserMessageParam(role="user", content="Tell me about a person named John who is 30")], + response_format=Person, + temperature=0, + seed=42, + ) as stream: + response = stream.parse() + person = response.choices[0].message.parsed + self.assertIsInstance(person, Person) + + def test_structured_outputs_with_beta_client_streaming(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + with client.beta.chat.completions.stream( + model=model, + messages=[ChatCompletionUserMessageParam(role="user", content="Tell me about a person named John who is 30")], + response_format=Person, + temperature=0, + seed=42, + ) as stream: + response = stream.get_final_completion() # This will wait for the full response to be received + person = response.choices[0].message.parsed + self.assertIsInstance(person, Person) + + def test_responses(self, model=OPENAI_GPT_5_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + response = client.responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the capital of France?", + ) + self.assertIsInstance(response, Response) + + def test_responses_streaming(self, model=OPENAI_GPT_5_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + response = client.responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the capital of France?", + stream=True, + ) + assert isinstance(response, openai.Stream) + + + def test_responses_global(self, model=OPENAI_GPT_5_TEST_MODEL): + from gen_ai_hub.proxy.native.openai import responses + response = responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the capital of France?", + ) + self.assertIsInstance(response, Response) + + def test_responses_structured_outputs(self, model=OPENAI_GPT_5_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + response = client.responses.parse( + model=model, + input="Tell me about John Doe aged 30.", + text_format=Person + ) + person = response.output_parsed + self.assertIsInstance(person, Person) + + @unittest.skip("Web search tool temporary is not supported.") + def test_responses_with_web_search_tool(self, model=OPENAI_GPT_5_TEST_MODEL): + client = OpenAI(proxy_client=self.proxy_client, max_retries=10) + response = client.responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the current weather in Tbilisi?", + tools=[{"type": "web_search_preview"}] + ) + self.assertIsInstance(response, Response) + + @staticmethod + def _get_test_messages(): + return [{ + 'role': 'user', + 'content': 'Does Azure OpenAI support customer managed keys?' + }, { + 'role': 'assistant', + 'content': 'Yes, customer managed keys are supported by Azure OpenAI.' + }, { + 'role': 'user', + 'content': 'Do other Azure Cognitive Services support this too?' + }] + + def test_cohere_command_a_reasoning(self, model=COHERE_COMMAND_A_TEST_MODEL): + messages = self._get_test_messages() + # Note: temperature should be automatically removed for reasoning models + kwargs = dict(model_name=model, messages=messages, temperature=0.5, seed=42) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + self.assertIsNotNone(response.model_extra['message']['content'][0]) + self.assertIsNotNone(response.model_extra['message']['content'][1]) + + def test_function_calling(self): + student_custom_functions = [ + { + 'name': 'extract_student_info', + 'description': 'Get the student information from the body of the input text', + 'parameters': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'Name of the person' + }, + 'major': { + 'type': 'string', + 'description': 'Major subject.' + }, + 'school': { + 'type': 'string', + 'description': 'The university name.' + } + } + } + } + ] + student_1_description = "David Nguyen is a sophomore majoring in computer science at Stanford University. He is Asian American and has a 3.8 GPA. David is known for his programming skills and is an active member of the university's Robotics Club. He hopes to pursue a career in artificial intelligence after graduating." + + kwargs = dict( + model=OPENAI_GPT_O4_MINI_TEST_MODEL, + messages=[{'role': 'user', 'content': student_1_description}], + functions=student_custom_functions, + function_call='auto', + temperature=0, + seed=42 + ) + response = OpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create(**kwargs) + # Loading the response as a JSON object + func_response = response.choices[0].message.function_call.arguments + self.assertEqual(response.choices[0].finish_reason, 'function_call') + self.assertEqual(response.choices[0].message.function_call.name, 'extract_student_info') + import ast + func_response_dict = ast.literal_eval(func_response) + self.assertTrue(all(k in func_response for k in func_response_dict)) + + +@unittest.skipIf(no_openai, 'openai not installed') +class AsyncOpenAITests(TestCaseAICoreSetupMixin, unittest.IsolatedAsyncioTestCase): + + async def test_async_embedding(self, model=OPENAI_EMBEDDING_TEST_MODEL): + kwargs = {'input': 'Your text string goes here', 'model_name': model} + response = await AsyncOpenAI(max_retries=10).embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + response = await AsyncOpenAI(max_retries=10).with_raw_response.embeddings.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + async def test_async_nvidia_embedding(self, model=NVIDIA_EMBEDDING_TEST_MODEL): + kwargs = { + 'input': 'Your text string goes here', + 'model_name': model, + 'extra_body': {'input_type': 'query'} # NVIDIA embedding model requires input_type + } + response = await AsyncOpenAI(max_retries=10).embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + response = await AsyncOpenAI(max_retries=10).with_raw_response.embeddings.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + @parameterized.expand( + [ + OPENAI_GPT_5_TEST_MODEL_NANO, + PERPLEXITY_SONAR_DEEP_RESEARCH_TEST_MODEL + ] + ) + async def test_async_chat_completion(self, model=OPENAI_GPT_5_TEST_MODEL_NANO): + prompt = 'Why is the sky blue?' + kwargs = dict(model_name=model, messages=[{'role': 'user', 'content': prompt}], seed=42) + response = await AsyncOpenAI(max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + response = await AsyncOpenAI(max_retries=10).with_raw_response.chat.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + async def test_async_chat_completion_with_model_version_param(self, model=OPENAI_GPT_5_TEST_MODEL_NANO): + prompt = 'Why is the sky blue?' + kwargs = dict(model_name=model, messages=[{'role': 'user', 'content': prompt}], seed=42, model_version="latest") + response = await AsyncOpenAI(max_retries=10).chat.completions.create(**kwargs) + self.assertIsInstance(response, ChatCompletion) + response = await AsyncOpenAI(max_retries=10).with_raw_response.chat.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + @parameterized.expand( + [ + OPENAI_GPT_O4_MINI_TEST_MODEL, + PERPLEXITY_TEST_MODEL + ] + ) + async def test_async_chat_completion_streaming(self, model=OPENAI_GPT_O4_MINI_TEST_MODEL): + messages = OpenAITests._get_test_messages() + generator = await AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10).chat.completions.create( + model_name=model, + messages=messages, + stream=True) + self.assertIsInstance(generator, openai.AsyncStream) + chunks = [value async for value in generator] + self.assertTrue(all(isinstance(chunk, ChatCompletionChunk) for chunk in chunks)) + self.assertGreater(len(chunks), 1, 'Only one chunk received - stream seems to be buffered.') + + async def test_async_completion(self, model=MISTRAL_TEST_MODEL): + prompt = ["This is a test"] + kwargs = dict(model_name=model, prompt=prompt, temperature=0, seed=42) + response = await AsyncOpenAI(proxy_client=self.proxy_client).completions.create(**kwargs) + self.assertIsInstance(response, Completion) + response = await AsyncOpenAI(proxy_client=self.proxy_client).with_raw_response.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + async def test_async_structured_outputs(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + response = await client.chat.completions.parse( + model=model, + messages=[ChatCompletionUserMessageParam(role="user", content="Tell me about John Doe aged 30.")], + response_format=Person + ) + person = response.choices[0].message.parsed # Fully typed Person object + print(person) + self.assertIsInstance(person, Person) + + async def test_async_structured_outputs_with_streaming(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + """ + streaming structured outputs requires the full object to be returned. + For more information, see https://www.github.com/openai/openai-python#with_streaming_response + """ + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + async with client.chat.completions.with_streaming_response.parse( + model=model, + messages=[{"role": "user", "content": "Tell me about a person named John who is 30"}], + response_format=Person, + temperature=0, + seed=42, + ) as stream: + response = await stream.parse() + person = response.choices[0].message.parsed + self.assertIsInstance(person, Person) + + async def test_async_structured_outputs_with_beta_client_streaming(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + async with client.beta.chat.completions.stream( + model=model, + messages=[{"role": "user", "content": "Tell me about a person named John who is 30"}], + response_format=Person, + temperature=0, + seed=42, + ) as stream: + response = await stream.get_final_completion() # This will wait for the full response to be received + person = response.choices[0].message.parsed + self.assertIsInstance(person, Person) + + async def test_async_responses(self, model=OPENAI_GPT_5_TEST_MODEL): + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + response = await client.responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the capital of France?", + ) + self.assertIsInstance(response, Response) + + async def test_async_responses_streaming(self, model=OPENAI_GPT_5_TEST_MODEL): + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + response = await client.responses.create( + model=model, + instructions="You are a helpful assistant.", + input="What is the capital of France?", + stream=True, + ) + assert isinstance(response, openai.AsyncStream) + + async def test_async_responses_structured_outputs(self, model=OPENAI_GPT_5_TEST_MODEL): + client = AsyncOpenAI(proxy_client=self.proxy_client, max_retries=10) + response = await client.responses.parse( + model=model, + input="Tell me about John Doe aged 30.", + text_format=Person + ) + person = response.output_parsed + self.assertIsInstance(person, Person) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/integration_tests/native_clients/test_sap_rpt.py b/packages/gen/integration_tests/native_clients/test_sap_rpt.py new file mode 100644 index 0000000..80a75db --- /dev/null +++ b/packages/gen/integration_tests/native_clients/test_sap_rpt.py @@ -0,0 +1,236 @@ +import unittest + +from integration_tests.constants import SAP_RPT_1_SMALL_TEST_MODEL +from integration_tests.setup_aicore import TestCaseStandardSetupMixin +from gen_ai_hub.proxy.native.sap.client import RPTClient +from gen_ai_hub.proxy.native.sap.models import RPTRequest, RPTResponse, PredictionConfig, TargetColumn + +request_by_row_dict = { + "prediction_config": { + "target_columns": [ + { + "name": "COSTCENTER", + "prediction_placeholder": "[PREDICT]", + "task_type": "classification" + } + ] + }, + "index_column": "ID", + "rows": [ + { + "PRODUCT": "Couch", + "PRICE": 999.99, + "ORDERDATE": "28-11-2025", + "ID": "35", + "COSTCENTER": "[PREDICT]" + }, + { + "PRODUCT": "Office Chair", + "PRICE": 150.8, + "ORDERDATE": "02-11-2025", + "ID": "44", + "COSTCENTER": "Office Furniture" + }, + { + "PRODUCT": "Server Rack", + "PRICE": 2200.00, + "ORDERDATE": "01-11-2025", + "ID": "104", + "COSTCENTER": "Data Infrastructure" + } + ], + "data_schema": { + "PRODUCT": { + "dtype": "string" + }, + "PRICE": { + "dtype": "numeric" + }, + "ORDERDATE": { + "dtype": "date" + }, + "ID": { + "dtype": "string" + }, + "COSTCENTER": { + "dtype": "string" + } + } + } + +request_by_columns_dict = { + "prediction_config": { + "target_columns": [ + { + "name": "COSTCENTER", + "prediction_placeholder": "[PREDICT]", + "task_type": "classification" + } + ] + }, + "columns": { + "PRODUCT": ["Couch", "Office Chair", "Server Rack"], + "PRICE": [999.99, 150.8, 2200.00], + "ORDERDATE": ["28-11-2025", "02-11-2025", "01-11-2025"], + "ID": ["35", "44", "104"], + "COSTCENTER": ["[PREDICT]", "Office Furniture", "Data Infrastructure"] + }, + "data_schema": { + "PRODUCT": { + "dtype": "string" + }, + "PRICE": { + "dtype": "numeric" + }, + "ORDERDATE": { + "dtype": "date" + }, + "ID": { + "dtype": "string" + }, + "COSTCENTER": { + "dtype": "string" + } + } +} + +rows_regression = [ + { + "PRODUCT": "Couch", + "PRICE": 999.99, + "ORDERDATE": "28-11-2025", + "ID": "35", + "DISCOUNT_RATE": "[PREDICT]", + }, + { + "PRODUCT": "Office Chair", + "PRICE": 150.80, + "ORDERDATE": "02-11-2025", + "ID": "44", + "DISCOUNT_RATE": 0.12, + }, + { + "PRODUCT": "Server Rack", + "PRICE": 2200.00, + "ORDERDATE": "01-11-2025", + "ID": "104", + "DISCOUNT_RATE": 0.05, + }, + { + "PRODUCT": "Standing Desk", + "PRICE": 640.00, + "ORDERDATE": "05-11-2025", + "ID": "205", + "DISCOUNT_RATE": 0.10, + }, + { + "PRODUCT": "Monitor 27 inch", + "PRICE": 289.99, + "ORDERDATE": "08-11-2025", + "ID": "306", + "DISCOUNT_RATE": "[PREDICT]", + }, +] + +class RPTClientTests(TestCaseStandardSetupMixin, unittest.TestCase): + + def setUp(self) -> None: + self.client = RPTClient(proxy_client=self.proxy_client) + + def test_client_find_url_by_model_name(self): + url = self.client._get_url(model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsNotNone(url) + + def test_client_find_url_by_model_name_and_version(self): + url = self.client._get_url(model_name=SAP_RPT_1_SMALL_TEST_MODEL, model_version="latest") + self.assertIsNotNone(url) + + def test_client_find_url_by_config_name(self): + url = self.client._get_url(config_name="sap-rpt-1-small-latest") + self.assertIsNotNone(url) + + def test_client_find_url_with_invalid_model_name(self): + with self.assertRaises(ValueError): + self.client._get_url(model_name="invalid-model-name") + + def test_predict_by_row(self): + body = RPTRequest(**request_by_row_dict) + response = self.client.predict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.predictions[0]["ID"], "35") + self.assertEqual(response.metadata.num_columns, 5) + self.assertEqual(response.metadata.num_predictions, 1) + self.assertIn("COSTCENTER", response.predictions[0].model_dump()) + + def test_predict_by_columns(self): + body = RPTRequest(**request_by_columns_dict) + response = self.client.predict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.metadata.num_columns, 5) + self.assertEqual(response.metadata.num_predictions, 1) + self.assertIn("COSTCENTER", response.predictions[0].model_dump()) + self.assertNotIn("ID", response.predictions[0].model_dump()) + + def test_predict_with_api_url(self): + body = RPTRequest(**request_by_columns_dict) + deployment_url = self.client._get_url(model_name=SAP_RPT_1_SMALL_TEST_MODEL) + response = self.client.predict(body=body, deployment_url=deployment_url) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.metadata.num_columns, 5) + self.assertEqual(response.metadata.num_predictions, 1) + self.assertIn("COSTCENTER", response.predictions[0].model_dump()) + + def test_regression_prediction(self): + body = RPTRequest( + prediction_config=PredictionConfig( + target_columns=[ + TargetColumn(name="DISCOUNT_RATE", task_type="regression") + ]), + rows=rows_regression + ) + response = self.client.predict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.metadata.num_predictions, 2) + self.assertIn("DISCOUNT_RATE", response.predictions[0].model_dump()) + + def test_timeout_error(self): + body = RPTRequest( + prediction_config=PredictionConfig( + target_columns=[ + TargetColumn(name="DISCOUNT_RATE", task_type="regression") + ]), + rows=rows_regression + ) + + with self.assertRaises(Exception): + self.client.predict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL, timeout=0.001) + + +class AsyncRPTClientTests(TestCaseStandardSetupMixin, unittest.IsolatedAsyncioTestCase): + + def setUp(self) -> None: + self.client = RPTClient(proxy_client=self.proxy_client) + + async def test_apredict_by_row(self): + body = RPTRequest(**request_by_row_dict) + response = await self.client.apredict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.predictions[0]["ID"], "35") + self.assertEqual(response.metadata.num_columns, 5) + self.assertEqual(response.metadata.num_predictions, 1) + self.assertIn("COSTCENTER", response.predictions[0].model_dump()) + + async def test_apredict_by_columns(self): + body = RPTRequest(**request_by_columns_dict) + response = await self.client.apredict(body=body, model_name=SAP_RPT_1_SMALL_TEST_MODEL) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.metadata.num_columns, 5) + self.assertEqual(response.metadata.num_predictions, 1) + self.assertIn("COSTCENTER", response.predictions[0].model_dump()) + self.assertNotIn("ID", response.predictions[0].model_dump()) \ No newline at end of file diff --git a/packages/gen/integration_tests/orchestration/__init__.py b/packages/gen/integration_tests/orchestration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/orchestration/test_async.py b/packages/gen/integration_tests/orchestration/test_async.py new file mode 100644 index 0000000..9f75a14 --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_async.py @@ -0,0 +1,114 @@ +import unittest + +from httpx import TimeoutException + +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.response import OrchestrationResponse +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class AsyncLLMTest(OrchestrationServiceTestBase, unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + # Set up common variables for asynchronous tests. + # Replace the API URL with your actual test endpoint. + self.service = OrchestrationService(api_url=self.api_url) + self.config = OrchestrationConfig( + llm=LLM(name="gpt-4o-mini", parameters={"temperature": 0.0}), + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + defaults=[TemplateValue("name", "World")], + ), + ) + + async def test_async_invalid_llm_name(self): + """Test that an unknown LLM name causes an error asynchronously.""" + llm = LLM(name="unknown-llm") + config = OrchestrationConfig(template=self.config.template, llm=llm) + with self.assertRaises(OrchestrationError): + await self.service.arun(config=config) + + async def test_async_invalid_llm_version(self): + """Test that an invalid LLM version causes an error asynchronously.""" + llm = LLM(name="gpt-4o-mini", version="unknown") + config = OrchestrationConfig(template=self.config.template, llm=llm) + with self.assertRaises(OrchestrationError): + await self.service.arun(config=config) + + async def test_async_valid_llm(self): + """Test that a valid LLM returns a result asynchronously.""" + response = await self.service.arun(config=self.config) + self.assertTrue(response.orchestration_result.model.startswith(self.config.llm.name)) + + async def test_async_streaming(self): + """Test asynchronous streaming mode returns at least one chunk.""" + service = OrchestrationService(api_url=self.api_url, config=self.config) + chunks = [] + # astream() returns an asynchronous iterator. + async for chunk in await service.astream(): + chunks.append(chunk) + self.assertGreater(len(chunks), 0, "No streaming chunks were received.") + + async def test_async_streaming_with_invalid_options(self): + """Test that passing invalid stream options raises an error asynchronously.""" + with self.assertRaises(OrchestrationError): + # The invalid stream options should cause an error during the async call. + async for _ in await self.service.astream(config=self.config, stream_options={'unknown': 10}): + pass + + async def test_async_reuse_client(self): + """ + ensures the client is reused and not closed when making multiple requests + """ + service = OrchestrationService(api_url=self.api_url, config=self.config) + reusable_client = service.async_client + # First request + chunks1 = [] + async for chunk in await service.astream(): + chunks1.append(chunk) + self.assertFalse(reusable_client.is_closed) + + # Second request + chunks2 = [] + async for chunk in await service.astream(): + chunks2.append(chunk) + self.assertGreater(len(chunks2), 0, "No streaming chunks were received.") + + # ensure httpx client is reused + self.assertEqual(reusable_client, service.async_client) + + await service.aclose_http_connection() + self.assertTrue(reusable_client.is_closed) + + async def test_async_timeout_per_request(self): + """ + set low default timeout for reusable client, which leads to a timeout. + overwrite timeout with higher value via request and show that response is returned. + """ + self.service = OrchestrationService(self.api_url, timeout=0.1) + config = OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("You are a famous professor for theoretical physics."), + UserMessage("Elaborate on the relativity theory."), + ], + ), + llm=LLM(name="gpt-5-nano") + ) + + # First request - should time out + with self.assertRaises(TimeoutException): + await self.service.arun(config=config) + + # Second request - should succeed due to overwrite in request with higher timeout + result = await self.service.arun(config=config, timeout=300) + self.assertIsInstance(result, OrchestrationResponse) diff --git a/packages/gen/integration_tests/orchestration/test_base.py b/packages/gen/integration_tests/orchestration/test_base.py new file mode 100644 index 0000000..d8dc3e6 --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_base.py @@ -0,0 +1,60 @@ +import time +import unittest +from uuid import uuid4 + +from gen_ai_hub.proxy import get_proxy_client + +shared_api_url = None + + +def get_shared_api_url(): + global shared_api_url + if shared_api_url is None: + shared_api_url = initialize_orchestration_service() + return shared_api_url + + +def initialize_orchestration_service(): + client = get_proxy_client(proxy_version="gen-ai-hub").ai_core_client + deployment = get_or_create_deployment(client) + return deployment.deployment_url + + +def get_or_create_deployment(client, timeout=600): + deployments = client.deployment.query(scenario_id="orchestration").resources + deployments = [d for d in deployments if d.status.value == "RUNNING"] + + if deployments: + return deployments[0] + + config = get_or_create_configuration(client) + deployment_id = client.deployment.create(configuration_id=config.id).id + + deployment = client.deployment.get(deployment_id) + start = time.time() + while deployment.status.value != "RUNNING": + if time.time() - start > timeout: + raise TimeoutError("Timeout waiting for deployment to start.") + deployment = client.deployment.get(deployment_id) + time.sleep(10) + + return deployment + + +def get_or_create_configuration(client): + configs = client.configuration.query(scenario_id="orchestration").resources + if configs: + return configs[0] + + return client.configuration.create( + scenario_id="orchestration", + executable_id="orchestration", + name=f"orchestration-config-{str(uuid4())[:8]}", + ) + + +class OrchestrationServiceTestBase(unittest.TestCase): + + def setUp(self): + self.api_url = get_shared_api_url() + self.assertIsNotNone(self.api_url) diff --git a/packages/gen/integration_tests/orchestration/test_content_filtering.py b/packages/gen/integration_tests/orchestration/test_content_filtering.py new file mode 100644 index 0000000..7472eda --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_content_filtering.py @@ -0,0 +1,176 @@ +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.azure_content_filter import AzureThreshold, AzureContentFilter +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.content_filter import ( + ContentFilter, + ContentFilterProvider, +) +from gen_ai_hub.orchestration.models.content_filtering import InputFiltering, OutputFiltering, ContentFiltering +from gen_ai_hub.orchestration.models.llama_guard_3_filter import LlamaGuard38bFilter +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.template import Template +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestContentFilter(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.llm = LLM( + name="gpt-4o-mini", + version="latest", + parameters={"max_tokens": 50, "temperature": 0.0}, + ) + self.template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + ] + ) + self.service = OrchestrationService( + api_url=self.api_url, + config=OrchestrationConfig( + template=self.template, + llm=self.llm, + ), + ) + + def test_invalid_filter_provider(self): + content_filter = ContentFilter(provider="unknown", config={"key": "value"}) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + filtering=ContentFiltering(InputFiltering(filters=[content_filter])) + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_azure_filter_with_invalid_config(self): + content_filter = ContentFilter( + provider=ContentFilterProvider.AZURE, config={"key": "value"} + ) + + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter])) + + with self.assertRaises(OrchestrationError): + self.service.run() + + def test_valid_input_filtering_with_azure(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL) + + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter])) + + response = self.service.run() + + self.assertIsNotNone(response.module_results.input_filtering) + self.assertIsNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_valid_output_filtering_with_azure(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + self.service.config.filtering = ContentFiltering(output_filtering=OutputFiltering(filters=[content_filter])) + + response = self.service.run() + + self.assertIsNone(response.module_results.input_filtering) + self.assertIsNotNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_valid_input_and_output_filtering(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter]), + OutputFiltering(filters=[content_filter]) + ) + + response = self.service.run() + + self.assertIsNotNone(response.module_results.input_filtering) + self.assertIsNotNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_blocked_input_filtering_with_azure(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + self.service.config.template.messages.append(UserMessage("I hate you!.")) + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter])) + + with self.assertRaises(OrchestrationError): + self.service.run() + + def test_valid_input_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter])) + + response = self.service.run() + + self.assertIsNotNone(response.module_results.input_filtering) + self.assertIsNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_valid_output_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + + self.service.config.filtering = ContentFiltering(output_filtering=OutputFiltering(filters=[content_filter])) + + response = self.service.run() + + self.assertIsNone(response.module_results.input_filtering) + self.assertIsNotNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_valid_input_and_output_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter]), + OutputFiltering(filters=[content_filter]) + ) + + response = self.service.run() + + self.assertIsNotNone(response.module_results.input_filtering) + self.assertIsNotNone(response.module_results.output_filtering) + self.assertIsNotNone(response.orchestration_result.model) + + def test_blocked_input_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter(elections=True) + + user_message = UserMessage("We need to manipulate the elections.") + + self.service.config.template.messages.append(user_message) + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter])) + + with self.assertRaises(OrchestrationError): + self.service.run() + + def test_blocked_input_filtering_with_azure_and_llama(self): + content_filter_azure = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + content_filter_llama = LlamaGuard38bFilter(hate=True) + + self.service.config.template.messages.append(UserMessage("I hate you!.")) + self.service.config.filtering = ContentFiltering(InputFiltering(filters=[content_filter_azure, content_filter_llama])) + + with self.assertRaises(OrchestrationError): + self.service.run() diff --git a/packages/gen/integration_tests/orchestration/test_data_masking.py b/packages/gen/integration_tests/orchestration/test_data_masking.py new file mode 100644 index 0000000..729f25b --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_data_masking.py @@ -0,0 +1,111 @@ +import json +import unittest + +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.data_masking import DataMasking +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.sap_data_privacy_integration import SAPDataPrivacyIntegration, ProfileEntity, \ + MaskingMethod +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + +@retry_on_429_or_503_class() +class TestDataMasking(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4", + parameters={ + 'temperature': 0.0, + } + ) + self.template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ] + ) + + def run_data_masking_test(self, masking_method: MaskingMethod, assertion_func): + data_masking = DataMasking( + providers=[ + SAPDataPrivacyIntegration( + method=masking_method, + entities=[ + ProfileEntity.EMAIL + ] + ) + ]) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + data_masking=data_masking + ) + + sensitive_data = "something@hotmail.com" + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", f"My email is {sensitive_data}" + f"-----------------------------------" + f"DON'T check if anything is masked, " + f"repeat the previous sentence." + f"DON'T alter the format of the " + f"masked data." + ) + ]) + + assertion_func(sensitive_data, response.orchestration_result.choices[0].message.content) + self.assertIsNotNone(response.module_results.input_masking) + + if masking_method == MaskingMethod.ANONYMIZATION: + self.assertIsNone(response.module_results.output_unmasking) + else: + self.assertIsNotNone(response.module_results.output_unmasking) + + @unittest.skip("backend module unavailable") + def test_data_masking_with_anonymization(self): + self.run_data_masking_test(MaskingMethod.ANONYMIZATION, self.assertNotIn) + + @unittest.skip("backend module unavailable") + def test_data_masking_with_pseudonymization(self): + self.run_data_masking_test(MaskingMethod.PSEUDONYMIZATION, self.assertIn) + + @unittest.skip("backend module unavailable") + def test_data_masking_with_allowlist(self): + allow_listed_org = "SAP" + data_masking = DataMasking( + providers=[ + SAPDataPrivacyIntegration( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[ProfileEntity.ORG], + allowlist=[allow_listed_org] + ) + ] + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + data_masking=data_masking + ) + + response = self.service.run( + config=config, + template_values=[ + TemplateValue("user_query", f"My organization is {allow_listed_org}") + ] + ) + + # Verify that the allow-listed org is present in the masked template + masked_template = json.loads(response.module_results.input_masking.data['masked_template']) + self.assertIn(allow_listed_org, masked_template[1]['content'], + f"Allow-listed organization '{allow_listed_org}' should not be masked") + + diff --git a/packages/gen/integration_tests/orchestration/test_grounding.py b/packages/gen/integration_tests/orchestration/test_grounding.py new file mode 100644 index 0000000..30a3359 --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_grounding.py @@ -0,0 +1,210 @@ +import json +import unittest + +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.data_masking import DataMasking +from gen_ai_hub.orchestration.models.document_grounding import GroundingModule, GroundingType, DocumentGrounding, \ + DocumentGroundingFilter, GroundingFilterSearch, DataRepositoryType +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.sap_data_privacy_integration import SAPDataPrivacyIntegration, MaskingMethod, \ + ProfileEntity +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class, retry_on_429_or_503 + + +@retry_on_429_or_503_class() +class TestGrounding(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4o", + parameters={ + 'temperature': 0.0, + } + ) + self.template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("Question: {{?user_query}}\n Context: {{?grounding_response}}"), + ] + ) + + def test_no_filter(self): + """ + Run orchestration service with default / empty grounding configuration. + """ + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response") + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + grounding=grounding_config + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is the orchestration service?"), + ]) + + self.assertIn("orchestration service", response.orchestration_result.choices[0].message.content) + + def test_grounding_SAPHelp(self): + """ + This tests the grounding option "elastic search" which is enabled for SAP Help website. + The indexed search is used instead of embedding vectors. + This is the minimal setup required for a grounding use case. + """ + + filters = [ + DocumentGroundingFilter(id="SAPHelp", data_repository_type="help.sap.com") + ] + + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response", filters=filters) + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + grounding=grounding_config + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is SAP AI Core?"), + ]) + + print(response.orchestration_result.choices[0].message.content) + self.assertIn("SAP AI Core", response.orchestration_result.choices[0].message.content) + + def test_grounding_vector(self): + """ + Test grounding based on vector store created with Data API. + Metadata keys point to sources of the documents. + """ + metadata_keys = ['source', 'webUrl', 'title', 'mimeType', 'fileSuffix'] + filters = [DocumentGroundingFilter(id="s3-docs", + data_repositories=["46b508c9-e490-4808-893b-b8e3361c4213"], + search_config=GroundingFilterSearch(max_chunk_count=2), + data_repository_type=DataRepositoryType.VECTOR.value + ) + ] + + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response", filters=filters, + metadata_params= metadata_keys) + ) + + orchestration_template = Template( + messages=[ + SystemMessage("""Facility Solutions Company provides services to luxury residential complexes, apartments, + individual homes, and commercial properties such as office buildings, retail spaces, industrial facilities, and educational institutions. + Customers are encouraged to reach out with maintenance requests, service deficiencies, follow-ups, or any issues they need by email. + """), + UserMessage("""You are a helpful assistant for any queries for answering questions. + Answer the request by providing relevant answers that fit to the request. + Request: {{ ?user_query }} + Context:{{ ?grounding_response }} + """), + ] + ) + + config = OrchestrationConfig( + template=orchestration_template, + llm=self.llm, + grounding=grounding_config + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "Is there a complaint?"), + ]) + self.assertIsNotNone(response.module_results.grounding) + metadata = json.loads(response.module_results.grounding.data['grounding_result'])[0]['metadata'] + for key in metadata_keys: + self.assertIn(key, metadata.keys()) + self.assertIn("complaint", response.orchestration_result.choices[0].message.content) + + @unittest.skip("Required setting up sharepoint.") + def test_grounding_sharepoint(self): # technical user for sharepoint not available + pass + + def test_grounding_with_data_masking_enabled(self): + filters = [ + DocumentGroundingFilter(id="SAPHelp", data_repository_type="help.sap.com") + ] + + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response", filters=filters) + ) + + data_masking = DataMasking( + providers=[ + SAPDataPrivacyIntegration( + method=MaskingMethod.ANONYMIZATION, + entities=[ + ProfileEntity.ORG + ], + mask_grounding_input=True + ) + ]) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + grounding=grounding_config, + data_masking=data_masking + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is SAP AI Core?"), + ]) + + self.assertIsNotNone(response.module_results.input_masking.data.get('masked_grounding_input')) + + def test_grounding_with_data_masking_disabled(self): + filters = [ + DocumentGroundingFilter(id="SAPHelp", data_repository_type="help.sap.com") + ] + + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response", filters=filters) + ) + + data_masking = DataMasking( + providers=[ + SAPDataPrivacyIntegration( + method=MaskingMethod.ANONYMIZATION, + entities=[ + ProfileEntity.ORG + ], + mask_grounding_input=False + ) + ]) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + grounding=grounding_config, + data_masking=data_masking + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is SAP AI Core?"), + ]) + + self.assertIsNone(response.module_results.input_masking.data.get('masked_grounding_input')) diff --git a/packages/gen/integration_tests/orchestration/test_llm.py b/packages/gen/integration_tests/orchestration/test_llm.py new file mode 100644 index 0000000..31b2124 --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_llm.py @@ -0,0 +1,96 @@ +from parameterized import parameterized + +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage +from gen_ai_hub.orchestration.models.template import Template +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + +@retry_on_429_or_503_class() +class TestLLM(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + ] + ) + + def test_invalid_llm_name(self): + + llm = LLM( + name="unknown-llm", + ) + + config = OrchestrationConfig( + template=self.template, + llm=llm, + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_invalid_llm_version(self): + + llm = LLM( + name="gpt-4o-mini", + version="unknown", + ) + + config = OrchestrationConfig( + template=self.template, + llm=llm, + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_invalid_llm_parameters(self): + + llm = LLM( + name="gpt-4o-mini", + parameters={ + "unknown_parameter": "value", + }, + ) + + config = OrchestrationConfig( + template=self.template, + llm=llm, + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + @parameterized.expand( + [ + # "gpt-4", + "gpt-4o", + "gpt-4o-mini", + "gemini-2.5-flash", + ] + ) + def test_valid_llm(self, name="gpt-4o-mini"): + + llm = LLM( + name=name, + parameters={ + 'temperature': 0.0, + } + ) + + config = OrchestrationConfig( + template=self.template, + llm=llm, + ) + + response = self.service.run(config=config) + + self.assertTrue(response.orchestration_result.model.startswith(llm.name)) + + diff --git a/packages/gen/integration_tests/orchestration/test_service.py b/packages/gen/integration_tests/orchestration/test_service.py new file mode 100644 index 0000000..60dd228 --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_service.py @@ -0,0 +1,168 @@ +from httpx import TimeoutException +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.response import OrchestrationResponse +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503 + + +class TestService(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(self.api_url) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_service_request_with_default_config(self): + config = OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + defaults=[TemplateValue(name="name", value="Integration Test")], + ), + llm=LLM( + name="gemini-2.5-flash", + parameters={ + 'temperature': 0.0, + } + ), + ) + + service = OrchestrationService(api_url=self.api_url, config=config) + + response = service.run() + + self.assertEqual( + response.module_results.templating[1].content, "Hello, Integration Test!" + ) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_service_with_inference_config(self): + config = OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + ), + llm=LLM( + name="gemini-2.5-flash", + parameters={ + 'temperature': 0.0, + } + ), + ) + + service = OrchestrationService(api_url=self.api_url, config=config) + + config.llm.name = "gemini-2.5-flash" + + response = service.run( + config=config, template_values=[TemplateValue("name", "World")] + ) + + self.assertTrue( + response.orchestration_result.model.startswith("gemini-2.5-flash") + ) + self.assertEqual(response.module_results.templating[1].content, "Hello, World!") + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_service_with_history(self): + response = self.service.run( + config=OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("This is a system message."), + ], + ), + llm=LLM( + name="gemini-2.5-flash", + parameters={ + 'temperature': 0.0, + } + ), + ), + history=[ + UserMessage("Hello, World!"), + UserMessage("How are you?"), + UserMessage("What is your name?"), + ], + ) + + self.assertEqual(len(response.module_results.templating), 4) + self.assertEqual(response.module_results.templating[0].content, "Hello, World!") + self.assertEqual(response.module_results.templating[1].content, "How are you?") + self.assertEqual( + response.module_results.templating[2].content, "What is your name?" + ) + self.assertEqual( + response.module_results.templating[3].content, "This is a system message." + ) + self.assertEqual( + response.orchestration_result.model.startswith("gemini-2"), True + ) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_reuse_client(self): + """ + ensures the client is reused and not closed when making multiple requests + """ + reusable_client = self.service.client + + #First request + config = OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + ), + llm=LLM( + name="gemini-2.5-flash", + parameters={ + 'temperature': 0.0, + } + ), + ) + self.service.run(config=config, template_values=[TemplateValue("name", "World")]) + self.assertFalse(reusable_client.is_closed) + + # Second request + self.service.run(config=config, template_values=[TemplateValue("name", "Earth")]) + + # ensure httpx client is reused + self.assertEqual(reusable_client, self.service.client) + + self.service.close_http_connection() + self.assertTrue(reusable_client.is_closed) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_timeout_per_request(self): + """ + set low default timeout for reusable client, which leads to a timeout. + overwrite timeout with higher value via request and show that response is returned. + """ + self.service = OrchestrationService(self.api_url, timeout=0.1) + config = OrchestrationConfig( + template=Template( + messages=[ + SystemMessage("You are a famous professor for theoretical physics."), + UserMessage("Elaborate on the relativity theory."), + ], + ), + llm=LLM(name="gpt-5-nano") + ) + + # First request - should time out + with self.assertRaises(TimeoutException): + self.service.run(config=config) + + # Second request - should succeed due to overwrite in request with higher timeout + result = self.service.run(config=config, timeout=300) + self.assertIsInstance(result, OrchestrationResponse) + diff --git a/packages/gen/integration_tests/orchestration/test_streaming.py b/packages/gen/integration_tests/orchestration/test_streaming.py new file mode 100644 index 0000000..2a000ed --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_streaming.py @@ -0,0 +1,160 @@ +import unittest +from typing import cast + +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.azure_content_filter import AzureContentFilter, AzureThreshold +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.content_filtering import OutputFiltering, ContentFiltering +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.response import OrchestrationResponseStreaming +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService +from integration_tests.constants import CLAUDE_4_5_SONNET_TEST_MODEL +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestStreaming(OrchestrationServiceTestBase): + + + def setUp(self): + super().setUp() + + self.template = Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + defaults=[TemplateValue(name="name", value="Integration Test")], + ) + self.llm = LLM( + name="gpt-4o-mini", + parameters={'temperature': 0.0} + ) + + def create_service(self, output_filtering=None, stream_options=None): + filter_config = None + if output_filtering: + filter_config = ContentFiltering(output_filtering=output_filtering) + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + filtering= filter_config, + ) + + if stream_options: + config.stream_options = stream_options + + return OrchestrationService(api_url=self.api_url, config=config) + + def test_streaming(self): + service = self.create_service() + + number_of_chunks = 0 + response_stream = service.stream() + for i, chunk in enumerate(response_stream): + chunk = cast(OrchestrationResponseStreaming, chunk) + if i == 0: + self.assertEqual(chunk.module_results.templating[1].content, "Hello, Integration Test!") + self.assertIsNone(chunk.module_results.llm) + else: + self.assertIsNotNone(chunk.module_results.llm) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + @unittest.skip("Internal server error") + def test_streaming_returns_token_usage(self): + self.llm = LLM(name=CLAUDE_4_5_SONNET_TEST_MODEL) + service = self.create_service() + + response_stream = service.stream() + for chunk in enumerate(response_stream): + response_streaming = cast(OrchestrationResponseStreaming, chunk)[1] + if response_streaming.orchestration_result.usage: + self.assertGreater(response_streaming.orchestration_result.usage.prompt_tokens, 0) + self.assertGreater(response_streaming.orchestration_result.usage.completion_tokens, 0) + self.assertGreater(response_streaming.orchestration_result.usage.total_tokens, 0) + self.assertGreater(response_streaming.module_results.llm.usage.total_tokens, 0) + + def test_streaming_with_stream_options(self, chunk_size=5): + service = self.create_service() + + response_stream = service.stream(stream_options={'chunk_size': chunk_size}) + number_of_chunks = 0 + for chunk in response_stream: + if chunk.orchestration_result.choices: + self.assertLessEqual(len(chunk.orchestration_result.choices[0].delta.content.split()), chunk_size) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + def test_streaming_with_invalid_stream_options(self): + service = self.create_service() + + with self.assertRaises(OrchestrationError): + for _ in service.stream(stream_options={'unknown': 10}): + pass + + def test_streaming_with_error_in_stream(self): + service = OrchestrationService( + api_url=self.api_url, + config=OrchestrationConfig( + llm=LLM( + name='gpt-4o-mini', + parameters={'temperature': 0.0, 'max_tokens': 100000} # This will exceed the token limit + ), + template=Template(messages=[UserMessage("Write a novel about maths")]) + ) + ) + + with self.assertRaises(OrchestrationError): + for _ in service.stream(): + pass + + def test_output_filtering_with_stream_options(self): + output_filtering = OutputFiltering( + filters=[ + AzureContentFilter( + hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + ) + ], + stream_options={'overlap': 10} + ) + + service = self.create_service(output_filtering=output_filtering) + response_stream = service.stream() + + number_of_chunks = 0 + for i, chunk in enumerate(response_stream): + chunk = cast(OrchestrationResponseStreaming, chunk) + if i == 0: + self.assertEqual(chunk.module_results.templating[1].content, "Hello, Integration Test!") + self.assertIsNone(chunk.module_results.llm) + elif i == 1: + self.assertIsNotNone(chunk.module_results.output_filtering) + self.assertIsNotNone(chunk.module_results.llm) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + def test_output_filtering_with_invalid_stream_options(self): + output_filtering = OutputFiltering( + filters=[ + AzureContentFilter( + hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + ) + ], + stream_options={'unknown': 10} + ) + + service = self.create_service(output_filtering) + + with self.assertRaises(OrchestrationError): + for _ in service.stream(): + pass diff --git a/packages/gen/integration_tests/orchestration/test_templating.py b/packages/gen/integration_tests/orchestration/test_templating.py new file mode 100644 index 0000000..7f13ffc --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_templating.py @@ -0,0 +1,563 @@ +import json +import os +import tempfile + +from PIL import Image +from typing import Dict, Any, List + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.multimodal_items import ImageItem +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage, Message, ToolMessage, AssistantMessage +from gen_ai_hub.orchestration.models.response_format import ResponseFormatJsonSchema +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.models.template_ref import TemplateRef +from gen_ai_hub.orchestration.models.tools import function_tool +from gen_ai_hub.orchestration.service import OrchestrationService +from gen_ai_hub.prompt_registry.client import PromptTemplateClient +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec, PromptTemplate +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +def check_response_from_referenced_template(response, content: str): + assert response.module_results.templating[0].content == content + assert len(response.orchestration_result.choices) > 0 + + +class TestTemplating(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4o-mini", + version="latest", + parameters={ + "max_tokens": 50, + "temperature": 0.0, + }, + ) + + def test_templating_with_default(self): + default = TemplateValue(name="user_query", value="Why is the sky blue?") + + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + defaults=[default], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + response = self.service.run(config=config) + + self.assertEqual(response.module_results.templating[1].content, default.value) + self.assertIsNone(response.module_results.input_filtering) + self.assertIsNone(response.module_results.output_filtering) + self.assertTrue(response.orchestration_result.model.startswith(self.llm.name)) + + def test_templating_with_user_input(self): + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + user_input = TemplateValue(name="user_query", value="Why is the sky blue?") + + response = self.service.run( + config=config, + template_values=[user_input], + ) + + self.assertEqual( + response.module_results.templating[1].content, user_input.value + ) + self.assertIsNone(response.module_results.input_filtering) + self.assertIsNone(response.module_results.output_filtering) + self.assertTrue(response.orchestration_result.model.startswith(self.llm.name)) + + def test_templating_with_no_messages(self): + template = Template( + messages=[], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_templating_with_invalid_message(self): + template = Template( + messages=[Message(role="unknown-role", content="Hello, world!")], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_templating_by_reference(self): + # create prompt template + prompt_template_scenario = "scenario_template_by_reference" + prompt_template_name = "prompt_template_by_reference" + prompt_template_version = "1.0.0" + user_content = "You are a system under test." + tenant_scoped_prompt_client = GenAIHubProxyClient(resource_group="") + prompt_template_client = PromptTemplateClient(tenant_scoped_prompt_client) + spec = PromptTemplateSpec(template=[PromptTemplate(role="user", content=user_content)]) + # reference prompt template + prompt_template_id = prompt_template_client.create_prompt_template(scenario=prompt_template_scenario, + name=prompt_template_name, + version=prompt_template_version, + prompt_template_spec=spec).id + config = OrchestrationConfig( + template=TemplateRef.from_id(prompt_template_id=prompt_template_id), + llm=self.llm, + ) + + response = self.service.run(config=config) + + check_response_from_referenced_template(response, user_content) + + config = OrchestrationConfig( + template=TemplateRef.from_tuple(scenario=prompt_template_scenario, name=prompt_template_name, + version=prompt_template_version), + llm=self.llm + ) + + response = self.service.run(config=config) + + check_response_from_referenced_template(response, user_content) + + # clean up + prompt_template_client.delete_prompt_template_by_id(prompt_template_id) + + def test_templating_with_response_format_text(self): + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + response_format="text" + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + user_input = TemplateValue(name="user_query", value="Who was the first person on the moon?") + + response = self.service.run( + config=config, + template_values=[user_input], + ) + + result = response.orchestration_result.choices[0].message.content + self.assertIsInstance(result, str) + try: + json.loads(result) + self.fail(msg="Response should be a text.") + except json.JSONDecodeError: + # The error means result is not a JSON object, so the test passes in this block + pass + + def test_templating_with_response_format_json_object(self): + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + response_format="json_object" + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + user_input = TemplateValue(name="user_query", value="Who was the first person on the moon? in json") + + response = self.service.run( + config=config, + template_values=[user_input], + ) + + try: + parsed_result = json.loads(response.orchestration_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + + def test_templating_with_response_format_json_schema(self): + json_schema = { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + } + } + + exp_result = { + "firstName": "Neil", + "lastName": "Armstrong" + } + + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + response_format=ResponseFormatJsonSchema(name="person", description="person mapping", schema=json_schema) + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + user_input = TemplateValue(name="user_query", value="Who was the first person on the moon? in json") + + response = self.service.run( + config=config, + template_values=[user_input], + ) + + try: + parsed_result = json.loads(response.orchestration_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + self.assertEqual(parsed_result, exp_result) + + def test_templating_with_response_format_json_schema_strict(self): + json_schema = { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + }, + "additionalProperties": False, + "required" :["firstName", "lastName"] + } + + exp_result = { + "firstName": "Neil", + "lastName": "Armstrong" + } + + template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ], + response_format=ResponseFormatJsonSchema(name="person", description="person mapping", schema=json_schema, strict=True) + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + user_input = TemplateValue(name="user_query", value="Who was the first person on the moon?") + + response = self.service.run( + config=config, + template_values=[user_input], + ) + + try: + parsed_result = json.loads(response.orchestration_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + self.assertEqual(parsed_result, exp_result) + +@retry_on_429_or_503_class() +class TestTemplateWithTools(OrchestrationServiceTestBase): + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4o-mini", + version="latest", + parameters={ + "max_tokens": 200, + "temperature": 0.0, + }, + ) + + def test_sync_tool_call_loop(self): + @function_tool() + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + tool_map: Dict[str, Any] = { + "multiply": multiply, + } + + template = Template( + messages=[ + SystemMessage("You are a math assistant."), + UserMessage("What is {{?a}} times {{?b}}?"), + ], + tools=[multiply], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + template_values = [ + TemplateValue(name="a", value=3), + TemplateValue(name="b", value=7), + ] + + # First run: should trigger a tool call + response = self.service.run( + config=config, + template_values=template_values, + ) + + # Check tool_calls in the response + tool_calls = response.orchestration_result.choices[0].message.tool_calls + self.assertIsNotNone(tool_calls) + self.assertGreaterEqual(len(tool_calls), 1) + tool_call = tool_calls[0] + self.assertEqual(tool_call.function.name, "multiply") + self.assertEqual(json.loads(tool_call.function.arguments), {"a": 3, "b": 7}) + self.assertIsNotNone(tool_call.id) + + # Check new fields if present + self.assertTrue(hasattr(tool_call, "id")) + self.assertTrue(hasattr(tool_call.function, "arguments")) + self.assertTrue(hasattr(tool_call.function, "name")) + + # Simulate tool execution and build new history + history: List[Message] = [] + history.extend(response.module_results.templating) + + assistant_message = AssistantMessage( + content=response.orchestration_result.choices[0].message.content, + refusal=response.orchestration_result.choices[0].message.refusal, + tool_calls=response.orchestration_result.choices[0].message.tool_calls) + + self.assertIsNone(assistant_message.refusal) + self.assertTrue(assistant_message.tool_calls) # assert some tool calls are present + + history.append(assistant_message) + + for tool_call in tool_calls: + tool = tool_map[tool_call.function.name] + result = tool.execute(**tool_call.function.parse_arguments()) + self.assertEqual(result, 21) + tool_message = ToolMessage( + content=f"{result}", + tool_call_id=tool_call.id, + ) + self.assertEqual(tool_message.tool_call_id, tool_call.id) + self.assertEqual(tool_message.content, str(result)) + self.assertEqual(tool_message.role, "tool") + history.append(tool_message) + + # Second run: should return the final answer + response2 = self.service.run( + config=config, + template_values=template_values, + history=history, + ) + + final_content = response2.orchestration_result.choices[0].message.content + self.assertIn("21", str(final_content)) + + tool_calls2 = response2.orchestration_result.choices[0].message.tool_calls + self.assertFalse(tool_calls2) + + def test_streaming_two_tool_call_buffering(self): + @function_tool() + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + @function_tool() + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + template = Template( + messages=[ + SystemMessage("You are a math assistant."), + UserMessage("What is 3 * 12? Also, what is 11 + 49?"), + ], + tools=[multiply, add], + ) + + config = OrchestrationConfig( + template=template, + llm=self.llm, + ) + + # Start streaming + stream = self.service.stream(config=config) + + final_tool_calls = {} + + for chunk in stream: + for tool_call in chunk.orchestration_result.choices[0].delta.tool_calls or []: + index = tool_call.index + + if index not in final_tool_calls: + final_tool_calls[index] = tool_call + else: + # Concatenate arguments if split across chunks + final_tool_calls[index].function.arguments += tool_call.function.arguments + + self.assertEqual(len(final_tool_calls), 2) + + multiply_call = next( + call for call in final_tool_calls.values() + if call.function.name == "multiply" + ) + self.assertIsNotNone(multiply_call.id) + + add_call = next( + call for call in final_tool_calls.values() + if call.function.name == "add" + ) + self.assertIsNotNone(add_call.id) + + self.assertEqual( + json.loads(multiply_call.function.arguments), {"a": 3, "b": 12} + ) + + self.assertEqual( + json.loads(add_call.function.arguments), {"a": 11, "b": 49} + ) + +@retry_on_429_or_503_class() +class TestMultimodalTemplating(OrchestrationServiceTestBase): + @classmethod + def setUpClass(cls): + cls.temp_dir = tempfile.TemporaryDirectory() + cls.image_path = os.path.join(cls.temp_dir.name, "test_image.png") + img = Image.new("RGB", (10, 10), color="red") + img.save(cls.image_path) + + @classmethod + def tearDownClass(cls): + cls.temp_dir.cleanup() + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4o", + version="latest", + parameters={ + "max_tokens": 50, + "temperature": 0.0, + }, + ) + + def test_image_from_url(self): + data_url = ( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==' + ) + image_item = ImageItem(url=data_url) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(messages=[UserMessage(multimodal_content)]) + config = OrchestrationConfig(template=template, llm=self.llm) + response = self.service.run(config=config) + + self.assertIn("red", response.content.lower()) + + def test_image_from_file(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(messages=[UserMessage(multimodal_content)]) + config = OrchestrationConfig(template=template, llm=self.llm) + response = self.service.run(config=config) + + self.assertIn("red", response.content.lower()) + + def test_only_image(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item] + + template = Template(messages=[UserMessage(multimodal_content)]) + config = OrchestrationConfig(template=template, llm=self.llm) + response = self.service.run(config=config) + + self.assertTrue(response.content) + + def test_multi_text_parts_are_handled(self): + multimodal_content = [ + "This is a text message.", + "This is another text message.", + ] + template = Template(messages=[UserMessage(multimodal_content)]) + config = OrchestrationConfig(template=template, llm=self.llm) + response = self.service.run(config=config) + + self.assertEqual( + len(response.module_results.templating[0].content), 2 + ) + self.assertTrue(response.content) + + def test_multimodal_input_streaming(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(messages=[UserMessage(multimodal_content)]) + config = OrchestrationConfig(template=template, llm=self.llm) + response = self.service.stream(config=config) + + message = '' + + for chunk in response: + if chunk.orchestration_result.choices: + message += chunk.orchestration_result.choices[0].delta.content + + self.assertIn("red", message.lower()) diff --git a/packages/gen/integration_tests/orchestration/test_translation.py b/packages/gen/integration_tests/orchestration/test_translation.py new file mode 100644 index 0000000..0a3092b --- /dev/null +++ b/packages/gen/integration_tests/orchestration/test_translation.py @@ -0,0 +1,125 @@ +import unittest + +from integration_tests.orchestration.test_base import OrchestrationServiceTestBase +from gen_ai_hub.orchestration.models.translation.sap_document_translation import SAPDocumentTranslation +from gen_ai_hub.orchestration.models.translation.translation import InputTranslationConfig, OutputTranslationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.template import Template +from gen_ai_hub.orchestration.service import OrchestrationService +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.template import TemplateValue +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestTranslation(OrchestrationServiceTestBase): + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLM( + name="gpt-4o", + parameters={ + 'temperature': 0.0, + } + ) + self.template = Template( + messages=[ + SystemMessage("You are a friendly assistant."), + UserMessage("{{?user_query}}"), + ] + ) + + def test_translation(self): + """ + Run orchestration service with translation configuration. + """ + + input_config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + output_config = OutputTranslationConfig(source_language="de-DE", target_language="en-US") + + translation_module = SAPDocumentTranslation( + input_translation_config=input_config, + output_translation_config=output_config + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + translation=translation_module + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is orchestration service?"), + ]) + + # Check the input translation output + self.assertRegex(response.module_results.input_translation.data["translated_template"], "Was ist .* Orchestrierungsservice") + # Check the output translation output + self.assertIn("choices", response.module_results.output_translation.data) + self.assertIn("orchestration", response.module_results.output_translation.data.get("choices")[0].get("message").get("content")) + # Check the orchestration result + self.assertIn("orchestration", response.orchestration_result.choices[0].message.content) + + def test_only_input_translation(self): + """ + Run orchestration service with translation configuration. + """ + + input_config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + + translation_module = SAPDocumentTranslation( + input_translation_config=input_config + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + translation=translation_module + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is orchestration service?"), + ]) + + # Check the input translation output + self.assertRegex(response.module_results.input_translation.data["translated_template"], "Was ist .* Orchestrierungsservice") + # Check the output translation output + self.assertIsNone(response.module_results.output_translation) + # Check the orchestration result + self.assertIn("Orchestrierungsservice", response.orchestration_result.choices[0].message.content) + + def test_only_output_translation(self): + """ + Run orchestration service with translation configuration. + """ + + output_config = OutputTranslationConfig(source_language="en-US", target_language="de-DE") + + translation_module = SAPDocumentTranslation( + output_translation_config=output_config + ) + + config = OrchestrationConfig( + template=self.template, + llm=self.llm, + translation=translation_module + ) + + response = self.service.run(config=config, + template_values=[ + TemplateValue("user_query", "What is orchestration service?"), + ]) + + # Check the input translation output + self.assertIsNone(response.module_results.input_translation) + # Check the output translation output + self.assertIn("choices", response.module_results.output_translation.data) + self.assertIn("Orchestrierungsservice", response.module_results.output_translation.data.get("choices")[0].get("message").get("content")) + # Check the orchestration result + self.assertIn("Orchestrierungsservice", response.orchestration_result.choices[0].message.content) + + + diff --git a/packages/gen/integration_tests/orchestration_v2/__init__.py b/packages/gen/integration_tests/orchestration_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/orchestration_v2/test_async.py b/packages/gen/integration_tests/orchestration_v2/test_async.py new file mode 100644 index 0000000..7ca790d --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_async.py @@ -0,0 +1,142 @@ +import unittest + +from httpx import TimeoutException + +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.config import (OrchestrationConfig, ModuleConfig, +CompletionRequestConfigurationReferenceByIdConfigRef, +CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.response import CompletionPostResponse +from gen_ai_hub.orchestration_v2.models.streaming import GlobalStreamOptions +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class AsyncLLMTest(OrchestrationServiceTestBase, unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + # Set up common variables for asynchronous tests. + # Replace the API URL with your actual test endpoint. + self.service = OrchestrationService(api_url=self.api_url) + prompt_template = PromptTemplatingModuleConfig( + model=LLMModelDetails(name="gpt-4o-mini", params={"temperature": 0.0}), + prompt=Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "World"}, + ), + ) + module_config = ModuleConfig(prompt_templating=prompt_template) + self.config = OrchestrationConfig(modules=module_config) + self.config_stream = OrchestrationConfig(modules=module_config, stream=GlobalStreamOptions(enabled=True)) + self.config_ref_id = CompletionRequestConfigurationReferenceByIdConfigRef(id="1234567890") + self.config_ref_name = CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef( + name="test", + version="1", + scenario="test" + ) + + async def test_async_invalid_llm_name(self): + """Test that an unknown LLM name causes an error asynchronously.""" + llm = LLMModelDetails(name="unknown-llm") + prompt_template = PromptTemplatingModuleConfig( + model=llm, + prompt=Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "World"}, + ), + ) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + with self.assertRaises(OrchestrationError): + await self.service.arun(config=config) + + async def test_async_invalid_llm_version(self): + """Test that an invalid LLM version causes an error asynchronously.""" + llm = LLMModelDetails(name="gpt-4o-mini", version="unknown") + prompt_template = PromptTemplatingModuleConfig( + model=llm, + prompt=Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "World"}, + ), + ) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + with self.assertRaises(OrchestrationError): + await self.service.arun(config=config) + + async def test_async_valid_llm(self): + """Test that a valid LLM returns a result asynchronously.""" + response = await self.service.arun(config=self.config) + self.assertTrue(response.final_result.model.startswith(self.config.modules.prompt_templating.model.name)) + + async def test_async_streaming(self): + """Test asynchronous streaming mode returns at least one chunk.""" + service = OrchestrationService(api_url=self.api_url, config=self.config_stream) + chunks = [] + # astream() returns an asynchronous iterator. + async for chunk in await service.astream(): + chunks.append(chunk) + self.assertGreater(len(chunks), 0, "No streaming chunks were received.") + + async def test_async_reuse_client(self): + """ + ensures the client is reused and not closed when making multiple requests + """ + service = OrchestrationService(api_url=self.api_url, config=self.config_stream) + reusable_client = service.async_client + # First request + chunks1 = [] + async for chunk in await service.astream(): + chunks1.append(chunk) + self.assertFalse(reusable_client.is_closed) + + # Second request + chunks2 = [] + async for chunk in await service.astream(): + chunks2.append(chunk) + self.assertGreater(len(chunks2), 0, "No streaming chunks were received.") + + # ensure httpx client is reused + self.assertEqual(reusable_client, service.async_client) + + await service.aclose_http_connection() + self.assertTrue(reusable_client.is_closed) + + async def test_async_timeout_per_request(self): + """ + set low default timeout for reusable client, which leads to a timeout. + overwrite timeout with higher value via request and show that response is returned. + """ + self.service = OrchestrationService(self.api_url, timeout=0.1) + + # First request - should time out + with self.assertRaises(TimeoutException): + await self.service.arun(config=self.config) + + # Second request - should succeed due to overwrite in request with higher timeout + result = await self.service.arun(config=self.config, timeout=300) + self.assertIsInstance(result, CompletionPostResponse) + + async def test_config_and_config_ref_provided_error_class_and_methode(self): + service = OrchestrationService(api_url=self.api_url, config=self.config) + with self.assertRaises(ValueError): + await service.arun(config_ref=self.config_ref_name) + + async def test_config_and_config_ref_provided_error_methode(self): + service = OrchestrationService(api_url=self.api_url) + with self.assertRaises(ValueError): + await service.arun(config=self.config, config_ref=self.config_ref_id) diff --git a/packages/gen/integration_tests/orchestration_v2/test_base.py b/packages/gen/integration_tests/orchestration_v2/test_base.py new file mode 100644 index 0000000..d8dc3e6 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_base.py @@ -0,0 +1,60 @@ +import time +import unittest +from uuid import uuid4 + +from gen_ai_hub.proxy import get_proxy_client + +shared_api_url = None + + +def get_shared_api_url(): + global shared_api_url + if shared_api_url is None: + shared_api_url = initialize_orchestration_service() + return shared_api_url + + +def initialize_orchestration_service(): + client = get_proxy_client(proxy_version="gen-ai-hub").ai_core_client + deployment = get_or_create_deployment(client) + return deployment.deployment_url + + +def get_or_create_deployment(client, timeout=600): + deployments = client.deployment.query(scenario_id="orchestration").resources + deployments = [d for d in deployments if d.status.value == "RUNNING"] + + if deployments: + return deployments[0] + + config = get_or_create_configuration(client) + deployment_id = client.deployment.create(configuration_id=config.id).id + + deployment = client.deployment.get(deployment_id) + start = time.time() + while deployment.status.value != "RUNNING": + if time.time() - start > timeout: + raise TimeoutError("Timeout waiting for deployment to start.") + deployment = client.deployment.get(deployment_id) + time.sleep(10) + + return deployment + + +def get_or_create_configuration(client): + configs = client.configuration.query(scenario_id="orchestration").resources + if configs: + return configs[0] + + return client.configuration.create( + scenario_id="orchestration", + executable_id="orchestration", + name=f"orchestration-config-{str(uuid4())[:8]}", + ) + + +class OrchestrationServiceTestBase(unittest.TestCase): + + def setUp(self): + self.api_url = get_shared_api_url() + self.assertIsNotNone(self.api_url) diff --git a/packages/gen/integration_tests/orchestration_v2/test_content_filtering.py b/packages/gen/integration_tests/orchestration_v2/test_content_filtering.py new file mode 100644 index 0000000..f478ca3 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_content_filtering.py @@ -0,0 +1,332 @@ +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.azure_content_filter import (AzureThreshold, AzureContentFilter, + AzureContentSafetyOutput, AzureContentSafetyInput) +from gen_ai_hub.orchestration_v2.models.llama_guard_3_filter import LlamaGuard38bFilter +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.content_filter import ( + AzureContentSafetyOutputFilterConfig, ContentFilter, ContentFilterProvider, + AzureContentSafetyInputFilterConfig, LlamaGuard38bFilterConfig +) +from gen_ai_hub.orchestration_v2.models.content_filtering import (InputFiltering, OutputFiltering, + FilteringModuleConfig) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + +@retry_on_429_or_503_class() +class TestContentFilter(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.llm = LLMModelDetails( + name="gpt-4o-mini", + version="latest", + params={"max_tokens": 50, "temperature": 0.0}, + ) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + ] + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + self.service = OrchestrationService( + api_url=self.api_url + ) + + def test_valid_input_filtering_with_azure(self): + content_filter = AzureContentSafetyInput(hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + prompt_shield=True) + + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_valid_output_filtering_with_azure(self): + content_filter = AzureContentSafetyOutput(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + content_filter_config = FilteringModuleConfig( + output= + OutputFiltering(filters=[ + AzureContentSafetyOutputFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_valid_input_and_output_filtering(self): + content_filter_output = AzureContentSafetyOutput(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + content_filter_input = AzureContentSafetyInput(hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + prompt_shield=True) + + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=content_filter_input) + ]), + output= + OutputFiltering(filters=[ + AzureContentSafetyOutputFilterConfig(config=content_filter_output) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_blocked_input_filtering_with_azure(self): + content_filter = AzureContentSafetyInput(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + self.template.template.append(UserMessage(content="I hate you!.")) + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_valid_input_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + LlamaGuard38bFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_valid_output_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + + content_filter_config = FilteringModuleConfig( + output= + OutputFiltering(filters=[ + LlamaGuard38bFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_valid_input_and_output_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + LlamaGuard38bFilterConfig(config=content_filter) + ]), + output= + OutputFiltering(filters=[ + LlamaGuard38bFilterConfig(config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_blocked_input_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter(elections=True) + self.template.template.append(UserMessage(content="We need to manipulate the elections.")) + content_filter_config = FilteringModuleConfig( + input=InputFiltering(filters=[LlamaGuard38bFilterConfig(config=content_filter)]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + + def test_blocked_input_filtering_with_azure_and_llama(self): + content_filter_azure = AzureContentSafetyInput(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + content_filter_llama = LlamaGuard38bFilter(hate=True) + self.template.template.append(UserMessage(content="I hate you!.")) + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=content_filter_azure), + LlamaGuard38bFilterConfig(config=content_filter_llama) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + +class TestContentFilterBackwardCompatibility(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.llm = LLMModelDetails( + name="gpt-4o-mini", + version="latest", + params={"max_tokens": 50, "temperature": 0.0}, + ) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + ] + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + self.service = OrchestrationService( + api_url=self.api_url + ) + + def test_valid_input_and_output_filtering(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.AZURE, config=content_filter) + ]), + output= + OutputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.AZURE, config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_blocked_input_filtering_with_azure(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + + self.template.template.append(UserMessage(content="I hate you!.")) + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.AZURE, config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_valid_input_and_output_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter() + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, config=content_filter) + ]), + output= + OutputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIsNotNone(response.intermediate_results.input_filtering) + self.assertIsNotNone(response.intermediate_results.output_filtering) + self.assertIsNotNone(response.final_result.model) + + def test_blocked_input_filtering_with_llama_guard38b(self): + content_filter = LlamaGuard38bFilter(elections=True) + self.template.template.append(UserMessage(content="We need to manipulate the elections.")) + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, config=content_filter) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + + def test_blocked_input_filtering_with_azure_and_llama(self): + content_filter_azure = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + self_harm=AzureThreshold.ALLOW_SAFE, + violence=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_SAFE) + content_filter_llama = LlamaGuard38bFilter(hate=True) + self.template.template.append(UserMessage(content="I hate you!.")) + content_filter_config = FilteringModuleConfig( + input= + InputFiltering(filters=[ + ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, config=content_filter_llama), + ContentFilter(type=ContentFilterProvider.AZURE, config=content_filter_azure) + ]) + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) \ No newline at end of file diff --git a/packages/gen/integration_tests/orchestration_v2/test_data_masking.py b/packages/gen/integration_tests/orchestration_v2/test_data_masking.py new file mode 100644 index 0000000..11aa296 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_data_masking.py @@ -0,0 +1,111 @@ +import json + +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.data_masking import (MaskingModuleConfig, MaskingProviderConfig, MaskingMethod, + DPIStandardEntity, ProfileEntity) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestDataMasking(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + params={ + 'temperature': 0.0, + } + ) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ] + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + + def test_data_masking_test(self): + data_masking_config = MaskingModuleConfig( + providers=[MaskingProviderConfig( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.EMAIL)] + )] + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, masking=data_masking_config) + + config = OrchestrationConfig(modules=module_config) + + sensitive_data = "something@hotmail.com" + + response = self.service.run(config=config, + placeholder_values={"user_query": f"My email is {sensitive_data}" + f"-----------------------------------" + f"DON'T check if anything is masked, " + f"repeat the previous sentence." + f"DON'T alter the format of the " + f"masked data." + } + ) + + self.assertIsNot(sensitive_data, response.final_result.choices[0].message.content) + self.assertIsNotNone(response.intermediate_results.input_masking) + + + def test_data_masking_with_allowlist(self): + allow_listed_org = "SAP" + data_masking_config = MaskingModuleConfig( + providers=[MaskingProviderConfig( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ORG)], + allowlist=[allow_listed_org] + )] + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, masking=data_masking_config) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run( + config=config, + placeholder_values={"user_query": f"My organisation is {allow_listed_org}"} + ) + + # Verify that the allow-listed org is present in the masked template + masked_template = json.loads(response.intermediate_results.input_masking.data['masked_template']) + self.assertIn(allow_listed_org, masked_template[1]['content'], + f"Allow-listed organization '{allow_listed_org}' should not be masked") + + def test_data_masking_test_backward_compatibility(self): + data_masking_config = MaskingModuleConfig( + masking_providers=[MaskingProviderConfig( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.EMAIL)] + )] + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, masking=data_masking_config) + + config = OrchestrationConfig(modules=module_config) + + sensitive_data = "something@hotmail.com" + + response = self.service.run(config=config, + placeholder_values={"user_query": f"My email is {sensitive_data}" + f"-----------------------------------" + f"DON'T check if anything is masked, " + f"repeat the previous sentence." + f"DON'T alter the format of the " + f"masked data." + } + ) + + self.assertIsNot(sensitive_data, response.final_result.choices[0].message.content) + self.assertIsNotNone(response.intermediate_results.input_masking) diff --git a/packages/gen/integration_tests/orchestration_v2/test_embeddings.py b/packages/gen/integration_tests/orchestration_v2/test_embeddings.py new file mode 100644 index 0000000..6436c1f --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_embeddings.py @@ -0,0 +1,261 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.embeddings import ( + EmbeddingsOrchestrationConfig, + EmbeddingsModuleConfigs, + EmbeddingsModelConfig, + EmbeddingsModelDetails, + EmbeddingsModelParams, + EmbeddingsInput, + EmbeddingsInputType, + EmbeddingsPostResponse, +) +from gen_ai_hub.orchestration_v2.models.data_masking import ( + MaskingModuleConfig, + MaskingProviderConfig, + MaskingMethod, + DPIStandardEntity, + ProfileEntity, +) +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestEmbeddings(OrchestrationServiceTestBase): + """Integration tests for embeddings endpoint.""" + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.base_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ) + ) + ) + + def test_embed_single_text(self): + """Test generating embedding for a single text.""" + response = self.service.embed( + config=self.base_config, + input=EmbeddingsInput(text="Hello World!") + ) + + self.assertIsInstance(response, EmbeddingsPostResponse) + self.assertIsNotNone(response.request_id) + self.assertEqual(len(response.final_result.data), 1) + self.assertEqual(response.final_result.data[0].index, 0) + self.assertIsInstance(response.final_result.data[0].embedding, list) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + # Verify usage info is returned + self.assertGreater(response.final_result.usage.prompt_tokens, 0) + self.assertGreater(response.final_result.usage.total_tokens, 0) + + def test_embed_batch_texts(self): + """Test generating embeddings for multiple texts in a single request.""" + documents = [ + "First document.", + "Second document.", + "Third document." + ] + response = self.service.embed( + config=self.base_config, + input=EmbeddingsInput(text=documents) + ) + + self.assertEqual(len(response.final_result.data), 3) + for i, result in enumerate(response.final_result.data): + self.assertEqual(result.index, i) + + def test_embed_with_model_params(self): + """Test embedding with custom model parameters (dimensions, normalize, encoding_format).""" + import math + from gen_ai_hub.orchestration_v2.models.embeddings import EmbeddingsEncodingFormat + + # Test dimensions + normalize (with default float encoding) + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-small", + params=EmbeddingsModelParams( + dimensions=256, + normalize=True + ) + ) + ) + ) + ) + + response = self.service.embed( + config=config, + input=EmbeddingsInput(text="Test model parameters") + ) + + embedding = response.final_result.data[0].embedding + # Verify dimensions + self.assertEqual(len(embedding), 256) + # Verify normalize: L2 norm should be ~1 + l2_norm = math.sqrt(sum(x * x for x in embedding)) + self.assertAlmostEqual(l2_norm, 1.0, places=3) + + # Test base64 encoding format + config_base64 = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-small", + params=EmbeddingsModelParams( + encoding_format=EmbeddingsEncodingFormat.BASE64 + ) + ) + ) + ) + ) + + response = self.service.embed( + config=config_base64, + input=EmbeddingsInput(text="Test base64 encoding") + ) + + # Base64 returns a string + self.assertIsInstance(response.final_result.data[0].embedding, str) + + def test_embed_with_input_types(self): + """Test embedding with different input type hints (document, query).""" + # Document type + response = self.service.embed( + config=self.base_config, + input=EmbeddingsInput( + text="SAP is a German multinational software company.", + type=EmbeddingsInputType.DOCUMENT + ) + ) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + + # Query type + response = self.service.embed( + config=self.base_config, + input=EmbeddingsInput( + text="What is SAP?", + type=EmbeddingsInputType.QUERY + ) + ) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + + def test_embed_with_data_masking(self): + """Test embedding with PII data masking and allowlist.""" + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.PERSON), + DPIStandardEntity(type=ProfileEntity.EMAIL), + ], + allowlist=["SAP"] + ) + ] + ) + ) + ) + + response = self.service.embed( + config=config, + input=EmbeddingsInput( + text="Contact John Smith at john@example.com about SAP." + ) + ) + + self.assertIsNotNone(response.intermediate_results) + self.assertIn("input_masking", response.intermediate_results) + # Verify allowlisted term is preserved + masked_input = response.intermediate_results["input_masking"]["data"]["masked_input"] + self.assertIn("SAP", masked_input) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + + + def test_embed_with_model_version(self): + """Test embedding with specific model version.""" + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-small", + version="latest" + ) + ) + ) + ) + + response = self.service.embed( + config=config, + input=EmbeddingsInput(text="Test model version") + ) + + self.assertIsInstance(response, EmbeddingsPostResponse) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + + def test_embed_invalid_model_name(self): + """Test that invalid model name raises an error.""" + from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError + + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="non-existent-model-xyz") + ) + ) + ) + + with self.assertRaises(OrchestrationError): + self.service.embed( + config=config, + input=EmbeddingsInput(text="This should fail") + ) + + +@retry_on_429_or_503_class() +class TestEmbeddingsAsync(OrchestrationServiceTestBase, unittest.IsolatedAsyncioTestCase): + """Async integration tests for embeddings endpoint.""" + + async def asyncSetUp(self): + self.service = OrchestrationService(api_url=self.api_url) + self.config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-small") + ) + ) + ) + + async def test_aembed_single_text(self): + """Test async embedding for a single text.""" + response = await self.service.aembed( + config=self.config, + input=EmbeddingsInput(text="Hello async world!") + ) + self.assertIsInstance(response, EmbeddingsPostResponse) + self.assertGreater(len(response.final_result.data[0].embedding), 0) + await self.service.aclose_http_connection() + + async def test_aembed_batch_texts(self): + """Test async embedding for batch texts.""" + response = await self.service.aembed( + config=self.config, + input=EmbeddingsInput(text=["First", "Second"]) + ) + self.assertEqual(len(response.final_result.data), 2) + await self.service.aclose_http_connection() + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/integration_tests/orchestration_v2/test_grounding.py b/packages/gen/integration_tests/orchestration_v2/test_grounding.py new file mode 100644 index 0000000..ec0ed6d --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_grounding.py @@ -0,0 +1,179 @@ +import json +import unittest + +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.data_masking import MaskingModuleConfig, DPIStandardEntity, ProfileEntity, \ + MaskingProviderConfig, MaskingMethod, MaskGroundingInput +from gen_ai_hub.orchestration_v2.models.document_grounding import (GroundingModuleConfig, GroundingType, DataRepositoryType, + DocumentGroundingConfig, DocumentGroundingFilter, + DocumentGroundingPlaceholders, GroundingSearchConfig) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import with_retry_on_missing_resource, retry_on_429_or_503 + + +class TestGrounding(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + params={ + 'temperature': 0.0, + } + ) + self.template = Template(template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="Question: {{?user_query}}\n Context: {{?grounding_response}}") + ] + ) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_no_filter(self): + """ + Run orchestration service with default / empty grounding configuration. + """ + grounding_config = GroundingModuleConfig( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGroundingConfig( + placeholders=DocumentGroundingPlaceholders(input=["user_query"], output="grounding_response") + ) + ) + + config = OrchestrationConfig( + modules=ModuleConfig(prompt_templating=PromptTemplatingModuleConfig(prompt=self.template, model=self.llm), + grounding=grounding_config) + ) + + response = self.service.run(config=config, + placeholder_values=({"user_query": "What is the orchestration service?"}), + ) + + self.assertIn("orchestration service", response.final_result.choices[0].message.content.lower()) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_grounding_SAPHelp(self): + """ + This tests the grounding option "elastic search" which is enabled for SAP Help website. + The indexed search is used instead of embedding vectors. + This is the minimal setup required for a grounding use case. + """ + + filters = [ + DocumentGroundingFilter(id="SAPHelp", data_repository_type="help.sap.com") + ] + + grounding_config = GroundingModuleConfig( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGroundingConfig( + placeholders=DocumentGroundingPlaceholders(input=["user_query"], output="grounding_response"), + filters=filters + ) + ) + + config = OrchestrationConfig( + modules=ModuleConfig(prompt_templating=PromptTemplatingModuleConfig(prompt=self.template, model=self.llm), + grounding=grounding_config) + ) + + response = self.service.run(config=config, + placeholder_values=( + {"user_query": "What is the SAP AI Core orchestration service?"}), + ) + + self.assertIn("SAP AI Core", response.final_result.choices[0].message.content) + + @with_retry_on_missing_resource(max_retries=3, delay=2.0) + def test_grounding_vector(self): + """ + Test grounding based on vector store created with Data API. + Metadata keys point to sources of the documents. + """ + metadata_keys = ['source', 'webUrl', 'title', 'mimeType', 'fileSuffix'] + filters = [DocumentGroundingFilter(id="s3-docs", + data_repositories=["46b508c9-e490-4808-893b-b8e3361c4213"], + search_config=GroundingSearchConfig(max_chunk_count=2), + data_repository_type=DataRepositoryType.VECTOR.value + ) + ] + + grounding = GroundingModuleConfig( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGroundingConfig(filters=filters, + placeholders={'input': ["user_query"], 'output': "grounding_response"}, + metadata_params=metadata_keys + ) + ) + + orchestration_template = Template(template=[ + SystemMessage(content="""Facility Solutions Company provides services to luxury residential complexes, apartments, + individual homes, and commercial properties such as office buildings, retail spaces, industrial facilities, and educational institutions. + Customers are encouraged to reach out with maintenance requests, service deficiencies, follow-ups, or any issues they need by email. + """), + UserMessage(content="""You are a helpful assistant for any queries for answering questions. + Answer the request by providing relevant answers that fit to the request. + Request: {{ ?user_query }} + Context:{{ ?grounding_response }} + """), + ] + ) + + config = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig(prompt=orchestration_template, model=self.llm), + grounding=grounding + ) + ) + + response = self.service.run(config=config, + placeholder_values=({"user_query": "Is there a complaint?"}) + ) + self.assertIsNotNone(response.intermediate_results.grounding) + metadata = json.loads(response.intermediate_results.grounding.data['grounding_result'])[0]['metadata'] + for key in metadata_keys: + self.assertIn(key, metadata.keys()) + self.assertIn("complaint", response.final_result.choices[0].message.content) + + @unittest.skip("Required setting up sharepoint.") + def test_grounding_sharepoint(self): # technical user for sharepoint not available + pass + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_grounding_with_data_masking_enabled(self): + filters = [ + DocumentGroundingFilter(id="SAPHelp", data_repository_type="help.sap.com") + ] + + grounding_config = GroundingModuleConfig( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGroundingConfig( + placeholders=DocumentGroundingPlaceholders(input=["user_query"], output="grounding_response"), + filters=filters + ) + ) + + data_masking = MaskingModuleConfig(providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ORG)], + mask_grounding_input=MaskGroundingInput(enabled=True) + ) + ]) + + config = OrchestrationConfig( + modules=ModuleConfig(prompt_templating=PromptTemplatingModuleConfig(prompt=self.template, model=self.llm), + grounding=grounding_config, + masking=data_masking + ) + ) + + response = self.service.run(config=config, + placeholder_values=({"user_query": "What is SAP AI Core?"}) + ) + + self.assertIsNotNone(response.intermediate_results.input_masking.data.get('masked_template')) + self.assertIn("MASKED_ORG", response.final_result.choices[0].message.content) diff --git a/packages/gen/integration_tests/orchestration_v2/test_llm_model_details.py b/packages/gen/integration_tests/orchestration_v2/test_llm_model_details.py new file mode 100644 index 0000000..3072887 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_llm_model_details.py @@ -0,0 +1,81 @@ +from parameterized import parameterized + +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestLLM(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + ] + ) + + def test_invalid_llm_name(self): + llm = LLMModelDetails( + name="unknown-llm", + ) + prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_invalid_llm_version(self): + llm = LLMModelDetails( + name="gpt-4o-mini", + version="unknown", + ) + prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_invalid_llm_parameters(self): + llm = LLMModelDetails( + name="gpt-4o-mini", + params={ + "unknown_parameter": "value", + }, + ) + prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + @parameterized.expand( + [ + "gpt-4o", + "gpt-4o-mini", + "gemini-2.5-flash", + ] + ) + def test_valid_llm(self, name="gpt-4o-mini"): + llm = LLMModelDetails( + name=name, + params={ + 'temperature': 0.0, + } + ) + + prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertTrue(response.final_result.model.startswith(llm.name)) diff --git a/packages/gen/integration_tests/orchestration_v2/test_service.py b/packages/gen/integration_tests/orchestration_v2/test_service.py new file mode 100644 index 0000000..f91456f --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_service.py @@ -0,0 +1,238 @@ +import requests +import time +from httpx import TimeoutException +from gen_ai_hub.orchestration_v2.models.config import (OrchestrationConfig, ModuleConfig, +CompletionRequestConfigurationReferenceByIdConfigRef, +CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.response import CompletionPostResponse +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestService(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(self.api_url) + self.template = Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "Integration Test"} + ) + self.llm = LLMModelDetails( + name="gemini-2.5-flash", + params={ + 'temperature': 0.0, + } + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + self.module_config = ModuleConfig(prompt_templating=self.prompt_template) + self.config_ref: dict = None + self.config_ref = self.create_config_ref() + self.config_ref_id = CompletionRequestConfigurationReferenceByIdConfigRef(id=self.config_ref["id"]) + self.config_ref_name = CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef( + name=self.config_ref["name"], + version=self.config_ref["version"], + scenario=self.config_ref["scenario"] + ) + + def tearDown(self): + headers = self.service.proxy_client.request_header + url = self.service.api_url.split("/inference")[0] + endpoint = f"{url}/registry/v2/orchestrationConfigs/{self.config_ref['id']}" + requests.delete(endpoint, headers=headers) + + def create_config_ref(self) -> dict: + if self.config_ref is not None: + return self.config_ref + headers = self.service.proxy_client.request_header + url = self.service.api_url.split("/inference")[0] + endpoint = f"{url}/registry/v2/orchestrationConfigs" + body = { + "scenario": "gen-ai-hub-sdk-config-ref-test-scenario", + "name": "gen-ai-hub-sdk-config-ref-test", + "version": "0.1.0", + "model_name": "gpt-4o", + "spec": { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + {"role": "user", "content": "Hello World"}, + ] + }, + "model": { + "name": "gpt-4o", + "version": "latest" + } + }, + }, + } + } + + # Retry logic to handle potential rate limiting or transient issues when creating config reference + max_retries = 3 + delay = 2.0 + for attempt in range(max_retries + 1): + try: + response = requests.post(endpoint, json=body, headers=headers) + response_json = response.json() + + # Check if response has required 'id' field + if 'id' not in response_json: + if attempt < max_retries: + time.sleep(delay) + delay *= 2 + continue + else: + raise KeyError(f"Response missing 'id' field: {response_json}") + + return response_json + except requests.exceptions.RequestException as e: + if attempt < max_retries: + time.sleep(delay) + delay *= 2 + else: + raise + + raise RuntimeError("Failed to create config reference after multiple attempts " + "due to rate limiting or missing 'id' in response.") + + def test_service_request_with_default_config(self): + service = OrchestrationService(api_url=self.api_url, config=OrchestrationConfig(modules=self.module_config)) + response = service.run() + + self.assertEqual( + response.intermediate_results.templating[1].content, "Hello, Integration Test!" + ) + + def test_service_request_with_list_of_config(self): + service = OrchestrationService(api_url=self.api_url, + config=OrchestrationConfig(modules=[self.module_config])) + response = service.run() + + self.assertEqual(response.intermediate_results.templating[1].content, "Hello, Integration Test!") + + def test_service_with_inference_config(self): + service = OrchestrationService(api_url=self.api_url) + config = OrchestrationConfig(modules=self.module_config) + + config.modules.prompt_templating.model.name = "gemini-2.5-flash" + + response = service.run( + config=config, placeholder_values={"name": "World"} + ) + + self.assertTrue( + response.final_result.model.startswith("gemini-2.5-flash") + ) + self.assertEqual(response.intermediate_results.templating[1].content, "Hello, World!") + + def test_service_with_history(self): + template = Template( + template=[ + SystemMessage(content="This is a system message.") + ] + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, model=self.llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run( + config=config, + history=[ + UserMessage(content="Hello, World!"), + UserMessage(content="How are you?"), + UserMessage(content="What is your name?"), + ] + ) + + self.assertEqual(len(response.intermediate_results.templating), 4) + self.assertEqual(response.intermediate_results.templating[0].content, "Hello, World!") + self.assertEqual(response.intermediate_results.templating[1].content, "How are you?") + self.assertEqual( + response.intermediate_results.templating[2].content, "What is your name?" + ) + self.assertEqual( + response.intermediate_results.templating[3].content, "This is a system message." + ) + self.assertTrue(response.final_result.model.startswith("gemini-2") + ) + + def test_reuse_client(self): + """ + ensures the client is reused and not closed when making multiple requests + """ + reusable_client = self.service.client + + #First request + config = OrchestrationConfig(modules=self.module_config) + self.service.run(config=config, placeholder_values={"name": "World"}) + self.assertFalse(reusable_client.is_closed) + + # Second request + self.service.run(config=config, placeholder_values={"name": "Sun"}) + + # ensure httpx client is reused + self.assertEqual(reusable_client, self.service.client) + + self.service.close_http_connection() + self.assertTrue(reusable_client.is_closed) + + def test_timeout_per_request(self): + """ + set low default timeout for reusable client, which leads to a timeout. + overwrite timeout with higher value via request and show that response is returned. + """ + template = Template( + template=[ + SystemMessage(content="You are a famous professor for theoretical physics."), + UserMessage(content="Elaborate on the relativity theory."), + ] + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, model=self.llm) + config = OrchestrationConfig(modules=ModuleConfig(prompt_templating=prompt_template)) + + # First request - should time out + with self.assertRaises(TimeoutException): + self.service.run(config=config, timeout=1) + + # Second request - should succeed due to overwrite in request with higher timeout + result = self.service.run(config=config, timeout=300) + self.assertIsInstance(result, CompletionPostResponse) + + def test_config_ref_by_id(self): + service = OrchestrationService(api_url=self.api_url, config_ref=self.config_ref_id) + response = service.run() + self.assertIsInstance(response, CompletionPostResponse) + self.assertEqual(response.intermediate_results.templating[0].content, "Hello World") + + def test_config_ref_by_snv(self): + service = OrchestrationService(api_url=self.api_url, config_ref=self.config_ref_name) + response = service.run() + self.assertIsInstance(response, CompletionPostResponse) + self.assertEqual(response.intermediate_results.templating[0].content, "Hello World") + + def test_config_and_config_ref_provided_error_class(self): + with self.assertRaises(ValueError): + OrchestrationService(api_url=self.api_url, + config=OrchestrationConfig(modules=self.module_config), + config_ref=self.config_ref_id) + + def test_config_and_config_ref_provided_error_class_and_methode(self): + service = OrchestrationService(api_url=self.api_url, config=OrchestrationConfig(modules=self.module_config)) + with self.assertRaises(ValueError): + service.run(config_ref=self.config_ref_name) + + def test_config_and_config_ref_provided_error_methode(self): + service = OrchestrationService(api_url=self.api_url) + with self.assertRaises(ValueError): + service.run(config=OrchestrationConfig(modules=self.module_config), config_ref=self.config_ref_id) + diff --git a/packages/gen/integration_tests/orchestration_v2/test_streaming.py b/packages/gen/integration_tests/orchestration_v2/test_streaming.py new file mode 100644 index 0000000..3b93929 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_streaming.py @@ -0,0 +1,161 @@ +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.azure_content_filter import (AzureContentSafetyOutput, AzureThreshold, + AzureContentFilter) +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.content_filter import (AzureContentSafetyOutputFilterConfig, ContentFilter, + ContentFilterProvider) +from gen_ai_hub.orchestration_v2.models.content_filtering import FilteringModuleConfig, OutputFiltering +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.response import StreamCompletionPostResponse +from gen_ai_hub.orchestration_v2.models.streaming import GlobalStreamOptions +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from integration_tests.constants import CLAUDE_4_5_SONNET_TEST_MODEL +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestStreaming(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + + self.template = Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "Integration Test"} + ) + self.llm = LLMModelDetails( + name="gpt-4o-mini", + params={'temperature': 0.0} + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + + def create_service(self, output_filtering=None, stream_options=None): + content_filter_config = None + if output_filtering: + content_filter_config = FilteringModuleConfig( + output=output_filtering + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config, + stream=GlobalStreamOptions(enabled=True, **(stream_options or {}))) + + return OrchestrationService(api_url=self.api_url, config=config) + + def test_streaming(self): + service = self.create_service() + + number_of_chunks = 0 + response_stream = service.stream() + for i, chunk in enumerate(response_stream): + if i == 0: + self.assertEqual(chunk.intermediate_results.templating[1].content, "Hello, Integration Test!") + self.assertIsNone(chunk.intermediate_results.llm) + else: + self.assertIsNotNone(chunk.intermediate_results.llm) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + def test_streaming_returns_token_usage(self): + self.llm = LLMModelDetails(name=CLAUDE_4_5_SONNET_TEST_MODEL) + service = self.create_service() + + response_stream = service.stream() + + for chunk in response_stream: + self.assertIsInstance(chunk, StreamCompletionPostResponse) + if chunk.final_result.usage: + self.assertGreater(chunk.final_result.usage.prompt_tokens, 0) + self.assertGreater(chunk.final_result.usage.completion_tokens, 0) + self.assertGreater(chunk.final_result.usage.total_tokens, 0) + + def test_streaming_with_stream_options(self, chunk_size=5): + service = self.create_service(stream_options={'chunk_size': chunk_size}) + + response_stream = service.stream() + number_of_chunks = 0 + for chunk in response_stream: + if chunk.final_result.choices: + self.assertLessEqual(len(chunk.final_result.choices[0].delta.content.split()), chunk_size) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + def test_streaming_with_error_in_stream(self): + template = Template(template=[UserMessage(content="Write a novel about maths")]) + llm = LLMModelDetails(name='gpt-4o-mini', + params={'temperature': 0.0, 'max_tokens': 100000} # This will exceed the token limit + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, model=llm) + module_config = ModuleConfig(prompt_templating=prompt_template) + config = OrchestrationConfig(modules=module_config, + stream=GlobalStreamOptions(enabled=True)) + service = OrchestrationService( + api_url=self.api_url, + config=config + ) + + with self.assertRaises(OrchestrationError): + for _ in service.stream(): + pass + + def test_output_filtering_with_stream_options(self): + output_filtering = OutputFiltering( + filters=[ + AzureContentSafetyOutputFilterConfig( + config=AzureContentSafetyOutput(hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + ) + ) + ], + stream_options={'overlap': 10} + ) + + service = self.create_service(output_filtering=output_filtering) + response_stream = service.stream() + + number_of_chunks = 0 + for i, chunk in enumerate(response_stream): + if i == 0: + self.assertEqual(chunk.intermediate_results.templating[1].content, "Hello, Integration Test!") + self.assertIsNone(chunk.intermediate_results.llm) + elif i == 1: + self.assertIsNotNone(chunk.intermediate_results.output_filtering) + self.assertIsNotNone(chunk.intermediate_results.llm) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") + + def test_output_filtering_with_stream_options_backward_compatibility(self): + output_filtering = OutputFiltering( + filters=[ + ContentFilter( + type=ContentFilterProvider.AZURE, + config=AzureContentFilter(hate=AzureThreshold.ALLOW_ALL, + self_harm=AzureThreshold.ALLOW_ALL, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_ALL, + ) + ) + ], + stream_options={'overlap': 10} + ) + + service = self.create_service(output_filtering=output_filtering) + response_stream = service.stream() + + number_of_chunks = 0 + for i, chunk in enumerate(response_stream): + if i == 0: + self.assertEqual(chunk.intermediate_results.templating[1].content, "Hello, Integration Test!") + self.assertIsNone(chunk.intermediate_results.llm) + elif i == 1: + self.assertIsNotNone(chunk.intermediate_results.output_filtering) + self.assertIsNotNone(chunk.intermediate_results.llm) + number_of_chunks += 1 + self.assertGreater(number_of_chunks, 1, "Only one chunk received - stream seems to be buffered.") \ No newline at end of file diff --git a/packages/gen/integration_tests/orchestration_v2/test_templating.py b/packages/gen/integration_tests/orchestration_v2/test_templating.py new file mode 100644 index 0000000..fc307e2 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_templating.py @@ -0,0 +1,595 @@ +import json +import os +import tempfile + +from PIL import Image +from typing import Dict, Any, List + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.multimodal_items import ImageItem +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage, AssistantMessage, ToolChatMessage, ChatMessage +from gen_ai_hub.orchestration_v2.models.response_format import ResponseFormatJsonObject, ResponseFormatText, ResponseFormatJsonSchema, JSONResponseSchema +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID, TemplateRefByScenarioNameVersion +from gen_ai_hub.orchestration_v2.models.tools import function_tool +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from gen_ai_hub.prompt_registry.client import PromptTemplateClient +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec, PromptTemplate +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from integration_tests.test_helpers import retry_on_429_or_503_class + + +def check_response_from_referenced_template(response, content: str): + assert response.intermediate_results.templating[0].content == content + assert len(response.final_result.choices) > 0 + + +@retry_on_429_or_503_class() +class TestTemplating(OrchestrationServiceTestBase): + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o-mini", + version="latest", + params={ + "max_tokens": 50, + "temperature": 0.0, + }, + ) + + def test_templating_with_default(self): + default = {"user_query": "Why is the sky blue?"} + + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ], + defaults=default + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config) + + self.assertEqual(response.intermediate_results.templating[1].content, default["user_query"]) + self.assertIsNone(response.intermediate_results.input_filtering) + self.assertIsNone(response.intermediate_results.output_filtering) + self.assertTrue(response.final_result.model.startswith(self.llm.name)) + + def test_templating_with_user_input(self): + user_input = {"user_query": "Why is the sky blue?"} + + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ] + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config, placeholder_values=user_input) + + self.assertEqual( + response.intermediate_results.templating[1].content, user_input["user_query"] + ) + self.assertIsNone(response.intermediate_results.input_filtering) + self.assertIsNone(response.intermediate_results.output_filtering) + self.assertTrue(response.final_result.model.startswith(self.llm.name)) + + def test_templating_with_no_messages(self): + template = Template( + template=[], + ) + + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError): + self.service.run(config=config) + + def test_templating_by_reference(self): + # create prompt template + prompt_template_scenario = "scenario_template_by_reference" + prompt_template_name = "prompt_template_by_reference" + prompt_template_version = "1.0.0" + user_content = "You are a system under test." + tenant_scoped_prompt_client = GenAIHubProxyClient(resource_group="") + prompt_template_client = PromptTemplateClient(tenant_scoped_prompt_client) + spec = PromptTemplateSpec(template=[PromptTemplate(role="user", content=user_content)]) + # reference prompt template + prompt_template_id = prompt_template_client.create_prompt_template(scenario=prompt_template_scenario, + name=prompt_template_name, + version=prompt_template_version, + prompt_template_spec=spec).id + + prompt_template = PromptTemplatingModuleConfig( + prompt=TemplateRef(template_ref=TemplateRefByID(id=prompt_template_id)), + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config) + + check_response_from_referenced_template(response, user_content) + + prompt_template = PromptTemplatingModuleConfig( + prompt=TemplateRef( + template_ref = TemplateRefByScenarioNameVersion( + scenario=prompt_template_scenario, + name=prompt_template_name, + version=prompt_template_version + ) + ), + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config) + + check_response_from_referenced_template(response, user_content) + + # clean up + prompt_template_client.delete_prompt_template_by_id(prompt_template_id) + + def test_templating_with_response_format_text(self): + user_input = {"user_query": "Why is the sky blue?"} + + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ], + response_format=ResponseFormatText() + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run( + config=config, + placeholder_values=user_input, + ) + + result = response.final_result.choices[0].message.content + self.assertIsInstance(result, str) + try: + json.loads(result) + self.fail(msg="Response should be a text.") + except json.JSONDecodeError: + # The error means result is not a JSON object, so the test passes in this block + pass + + def test_templating_with_response_format_json_object(self): + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ], + response_format=ResponseFormatJsonObject() + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + user_input = {"user_query":"Who was the first person on the moon? in json"} + + response = self.service.run( + config=config, + placeholder_values=user_input, + ) + + try: + parsed_result = json.loads(response.final_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + + def test_templating_with_response_format_json_schema(self): + json_schema = { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + } + } + + exp_result = { + "firstName": "Neil", + "lastName": "Armstrong" + } + + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ], + response_format=ResponseFormatJsonSchema( + json_schema=JSONResponseSchema( + name="person", description="person mapping", schema=json_schema + ), + ) + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + user_input = {"user_query": "Who was the first person on the moon? in json"} + + response = self.service.run( + config=config, + placeholder_values=user_input, + ) + + try: + parsed_result = json.loads(response.final_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + self.assertEqual(parsed_result, exp_result) + + def test_templating_with_response_format_json_schema_strict(self): + json_schema = { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + }, + "additionalProperties": False, + "required" :["firstName", "lastName"] + } + + exp_result = { + "firstName": "Neil", + "lastName": "Armstrong" + } + + template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ], + response_format=ResponseFormatJsonSchema( + json_schema=JSONResponseSchema( + name="person", description="person mapping", schema=json_schema, strict=True + ), + ) + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + user_input = {"user_query": "Who was the first person on the moon? in json"} + + response = self.service.run( + config=config, + placeholder_values=user_input, + ) + + try: + parsed_result = json.loads(response.final_result.choices[0].message.content) + except json.JSONDecodeError: + self.fail("Result of LLM is not a valid JSON object") + + self.assertIsInstance(parsed_result, dict) + self.assertEqual(parsed_result, exp_result) + +@retry_on_429_or_503_class() +class TestTemplateWithTools(OrchestrationServiceTestBase): + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + version="latest", + params={ + "max_tokens": 200, + "temperature": 0.0, + }, + ) + + def test_sync_tool_call_loop(self): + @function_tool() + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + tool_map: Dict[str, Any] = { + "multiply": multiply, + } + + template = Template( + template=[ + SystemMessage(content="You are a math assistant."), + UserMessage(content="What is {{?a}} times {{?b}}?"), + ], + tools=[multiply], + ) + prompt_template=PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + template_values = {"a": "3", "b": "7"} + + # First run: should trigger a tool call + response = self.service.run( + config=config, + placeholder_values=template_values, + ) + + # Check tool_calls in the response + tool_calls = response.final_result.choices[0].message.tool_calls + self.assertIsNotNone(tool_calls) + self.assertGreaterEqual(len(tool_calls), 1) + tool_call = tool_calls[0] + self.assertEqual(tool_call.function.name, "multiply") + self.assertEqual(json.loads(tool_call.function.arguments), {"a": 3, "b": 7}) + self.assertIsNotNone(tool_call.id) + + # Check new fields if present + self.assertTrue(hasattr(tool_call, "id")) + self.assertTrue(hasattr(tool_call.function, "arguments")) + self.assertTrue(hasattr(tool_call.function, "name")) + + # Simulate tool execution and build new history + history: List[ChatMessage] = [] + history.extend(response.intermediate_results.templating) + + assistant_message = AssistantMessage( + content=response.final_result.choices[0].message.content, + refusal=response.final_result.choices[0].message.refusal, + tool_calls=response.final_result.choices[0].message.tool_calls) + + self.assertIsNone(assistant_message.refusal) + self.assertTrue(assistant_message.tool_calls) # assert some tool calls are present + + history.append(assistant_message) + + for tool_call in tool_calls: + tool = tool_map[tool_call.function.name] + result = tool.execute(**tool_call.function.parse_arguments()) + self.assertEqual(result, 21) + tool_message = ToolChatMessage( + content=f"{result}", + tool_call_id=tool_call.id, + ) + self.assertEqual(tool_message.tool_call_id, tool_call.id) + self.assertEqual(tool_message.content, str(result)) + self.assertEqual(tool_message.role, "tool") + history.append(tool_message) + + # Second run: should return the final answer + response2 = self.service.run( + config=config, + placeholder_values=template_values, + history=history, + ) + + final_content = response2.final_result.choices[0].message.content + self.assertIn("21", str(final_content)) + + def test_streaming_two_tool_call_buffering(self): + @function_tool() + def multiply(a: int, b: int) -> int: + """Multiply two numbers.""" + return a * b + + @function_tool() + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + template = Template( + template=[ + SystemMessage(content="You are a math assistant."), + UserMessage(content="What is 3 * 12? Also, what is 11 + 49?"), + ], + tools=[multiply, add], + ) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config, stream={"enabled": True}) + + # Start streaming + stream = self.service.stream(config=config) + + final_tool_calls = {} + + for chunk in stream: + for tool_call in chunk.final_result.choices[0].delta.tool_calls or []: + index = tool_call.index + + if index not in final_tool_calls: + final_tool_calls[index] = tool_call + else: + # Concatenate arguments if split across chunks + final_tool_calls[index].function.arguments += tool_call.function.arguments + + self.assertEqual(len(final_tool_calls), 2) + + for call in final_tool_calls.values(): + if call.function.name == "multiply": + multiply_call = call + if call.function.name == "add": + add_call = call + + self.assertIsNotNone(multiply_call.id) + self.assertIsNotNone(add_call.id) + + self.assertEqual( + json.loads(multiply_call.function.arguments), {"a": 3, "b": 12} + ) + + self.assertEqual( + json.loads(add_call.function.arguments), {"a": 11, "b": 49} + ) + +@retry_on_429_or_503_class() +class TestMultimodalTemplating(OrchestrationServiceTestBase): + @classmethod + def setUpClass(cls): + cls.temp_dir = tempfile.TemporaryDirectory() + cls.image_path = os.path.join(cls.temp_dir.name, "test_image.png") + img = Image.new("RGB", (10, 10), color="red") + img.save(cls.image_path) + + @classmethod + def tearDownClass(cls): + cls.temp_dir.cleanup() + + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + version="latest", + params={ + "max_tokens": 50, + "temperature": 0.0, + }, + ) + + def test_image_from_url(self): + data_url = ( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==' + ) + image_item = ImageItem(url=data_url) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(template=[UserMessage(content=multimodal_content)]) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertIn("red", response.final_result.choices[0].message.content.lower()) + + def test_image_from_file(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(template=[UserMessage(content=multimodal_content)]) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config) + + self.assertIn("red", response.final_result.choices[0].message.content.lower()) + + def test_only_image(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item] + + template = Template(template=[UserMessage(content=multimodal_content)]) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config) + + self.assertIn("red", response.final_result.choices[0].message.content.lower()) + + def test_multi_text_parts_are_handled(self): + multimodal_content = [ + "This is a text message.", + "This is another text message.", + ] + template = Template(template=[UserMessage(content=multimodal_content)]) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config) + response = self.service.run(config=config) + + self.assertEqual( + len(response.intermediate_results.templating[0].content), 2 + ) + self.assertTrue(response.final_result.choices[0].message.content) + + def test_multimodal_input_streaming(self): + image_item = ImageItem.from_file(self.image_path) + multimodal_content = [image_item, "What color is this image?"] + + template = Template(template=[UserMessage(content=multimodal_content)]) + prompt_template = PromptTemplatingModuleConfig(prompt=template, + model=self.llm) + + module_config = ModuleConfig(prompt_templating=prompt_template) + + config = OrchestrationConfig(modules=module_config, stream={"enabled": True}) + response = self.service.stream(config=config) + + message = '' + + for chunk in response: + if chunk.final_result.choices: + message += chunk.final_result.choices[0].delta.content + + self.assertIn("red", message.lower()) diff --git a/packages/gen/integration_tests/orchestration_v2/test_translation.py b/packages/gen/integration_tests/orchestration_v2/test_translation.py new file mode 100644 index 0000000..f3f0063 --- /dev/null +++ b/packages/gen/integration_tests/orchestration_v2/test_translation.py @@ -0,0 +1,267 @@ +from integration_tests.orchestration_v2.test_base import OrchestrationServiceTestBase +from gen_ai_hub.orchestration_v2.models.translation import (TranslationModuleConfig, InputTranslationConfig, + OutputTranslationConfig, SAPDocumentTranslationInput, + SAPDocumentTranslationOutput, + SAPDocumentTranslationApplyToSelector, + TranslationConfig, SAPDocumentTranslation) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from integration_tests.test_helpers import retry_on_429_or_503_class + + +@retry_on_429_or_503_class() +class TestTranslation(OrchestrationServiceTestBase): + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + params={ + 'temperature': 0.0, + } + ) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ] + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + + def test_translation(self): + """ + Run orchestration service with translation configuration. + """ + + input_config = SAPDocumentTranslationInput( + config=InputTranslationConfig( + source_language="en-US", + target_language="de-DE" + )) + output_config = SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="de-DE", + target_language="en-US" + )) + + translation_config = TranslationModuleConfig( + input=input_config, + output=output_config + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config, + placeholder_values={"user_query": "What is orchestration service?"} + ) + + # Check the input translation output + self.assertRegex( + response.intermediate_results.input_translation.data["translated_template"], + "Was ist .* Orchestrierungsservice") + # Check the output translation output + self.assertIn("choices", response.intermediate_results.output_translation.data) + self.assertIn( + "orchestration", + response.intermediate_results.output_translation.data.get("choices")[0].get("message").get("content")) + # Check the orchestration result + self.assertIn("orchestration", response.final_result.choices[0].message.content) + + def test_only_input_translation(self): + """ + Run orchestration service with translation configuration. + """ + + input_config = SAPDocumentTranslationInput( + config=InputTranslationConfig( + source_language="en-US", + target_language="de-DE" + )) + + translation_config = TranslationModuleConfig( + input=input_config + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config, + placeholder_values={"user_query": "What is orchestration service?"} + ) + + # Check the input translation output + self.assertRegex( + response.intermediate_results.input_translation.data["translated_template"], + "Was ist .* Orchestrierungsservice") + # Check the output translation output + self.assertIsNone(response.intermediate_results.output_translation) + # Check the orchestration result + self.assertIn("Orchestrierungsservice", response.final_result.choices[0].message.content) + + def test_only_output_translation(self): + """ + Run orchestration service with translation configuration. + """ + + output_config = SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="en-US", target_language="de-DE" + )) + + translation_config = TranslationModuleConfig( + output=output_config + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config, + placeholder_values={"user_query": "What is orchestration service?"} + ) + + # Check the input translation output + self.assertIsNone(response.intermediate_results.input_translation) + # Check the output translation output + self.assertIn("choices", response.intermediate_results.output_translation.data) + self.assertIn( + "Orchestrierungsservice", + response.intermediate_results.output_translation.data.get("choices")[0].get("message").get("content")) + # Check the orchestration result + self.assertIn("Orchestrierungsservice", response.final_result.choices[0].message.content) + + def test_input_translation_apply_to_only_user_placeholder(self): + input_config = SAPDocumentTranslationInput( + config=InputTranslationConfig( + target_language="de-DE", + apply_to=[ + SAPDocumentTranslationApplyToSelector( + category="placeholders", + items=["user_query"], + source_language="en-US", + ) + ], + ), + ) + translation_config = TranslationModuleConfig(input=input_config) + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run( + config=config, + placeholder_values={"user_query": "What is orchestration service?"}, + ) + + translated = response.intermediate_results.input_translation.data["translated_placeholders"]["user_query"] + + self.assertRegex(translated, r"Was ist .*Orchestrierungsservice") + + def test_input_translation_translate_history(self): + input_config = SAPDocumentTranslationInput( + translate_messages_history=False, + config=InputTranslationConfig( + target_language="de-DE"), + ) + translation_config = TranslationModuleConfig(input=input_config) + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run( + config=config, + placeholder_values={"user_query": "What is orchestration service?"}, + history=[UserMessage(content="Hello, World!")], + ) + + translated = response.intermediate_results.input_translation.data["translated_template"] + + self.assertRegex(translated, r"Was ist .*Orchestrierungsservice") + self.assertNotIn(translated, "Welt") + + def test_output_translation_target_language_from_placeholder_selector(self): + output_config = SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="en-US", + target_language=SAPDocumentTranslationApplyToSelector( + category="placeholders", + items=["target_lang"], + source_language="en-US", + ), + ) + ) + + translation_config = TranslationModuleConfig(output=output_config) + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + with self.assertRaises(OrchestrationError) as ctx: + self.service.run( + config=config, + placeholder_values={ + "user_query": "What is orchestration service?", + "target_lang": "de-DE", + }, + ) + + self.assertIn("is not present in translation.input.config.apply_to", str(ctx.exception)) + + +@retry_on_429_or_503_class() +class TestTranslationBackwardCompatibility(OrchestrationServiceTestBase): + def setUp(self): + super().setUp() + self.service = OrchestrationService(api_url=self.api_url) + self.llm = LLMModelDetails( + name="gpt-4o", + params={ + 'temperature': 0.0, + } + ) + self.template = Template( + template=[ + SystemMessage(content="You are a friendly assistant."), + UserMessage(content="{{?user_query}}"), + ] + ) + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, model=self.llm) + + def test_translation(self): + """ + Run orchestration service with translation configuration. + """ + + input_config = SAPDocumentTranslation( + config=TranslationConfig( + source_language="en-US", + target_language="de-DE" + )) + output_config = SAPDocumentTranslation( + config=TranslationConfig( + source_language="de-DE", target_language="en-US" + )) + + translation_config = TranslationModuleConfig( + input=input_config, + output=output_config + ) + + module_config = ModuleConfig(prompt_templating=self.prompt_template, translation=translation_config) + config = OrchestrationConfig(modules=module_config) + + response = self.service.run(config=config, + placeholder_values={"user_query": "What is orchestration service?"} + ) + + # Check the input translation output + self.assertRegex( + response.intermediate_results.input_translation.data["translated_template"], + "Was ist .* Orchestrierungsservice") + # Check the output translation output + self.assertIn("choices", response.intermediate_results.output_translation.data) + self.assertIn("orchestration", + response.intermediate_results.output_translation.data.get("choices")[0].get("message").get("content")) + # Check the orchestration result + self.assertIn("orchestration", response.final_result.choices[0].message.content) diff --git a/packages/gen/integration_tests/prompt_registry/__init__.py b/packages/gen/integration_tests/prompt_registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/integration_tests/prompt_registry/test_client.py b/packages/gen/integration_tests/prompt_registry/test_client.py new file mode 100644 index 0000000..981e435 --- /dev/null +++ b/packages/gen/integration_tests/prompt_registry/test_client.py @@ -0,0 +1,278 @@ +import unittest +from typing import cast + +from ai_api_client_sdk.exception import AIAPIServerException + +from gen_ai_hub import GenAIHubProxyClient +from gen_ai_hub.prompt_registry.client import PromptTemplateClient, OrchestrationConfigClient +from gen_ai_hub.prompt_registry.models.prompt_template import (PromptTemplate, + PromptTemplateSpec, + PromptTemplateListResponse, + PromptTemplatePostResponse, + PromptTemplateGetResponse, + PromptTemplateDeleteResponse, + PromptTemplateSubstitutionResponse) +from gen_ai_hub.prompt_registry.models.orchestration_config import (OrchestrationConfigDeleteResponse, + OrchestrationConfigGetResponse, + OrchestrationConfigPostResponse, + OrchestrationConfigListResponse) +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.response_format import ResponseFormatJsonObject +from gen_ai_hub.orchestration_v2.models.tools import FunctionTool +from gen_ai_hub.orchestration_v2.models.multimodal_items import ImageItem + +from gen_ai_hub.proxy import get_proxy_client +from tests.mock import TEMPLATE_YAML, ORCHESTRATION_CONFIG_YAML +from integration_tests.test_helpers import retry_on_429_or_503 + + +class TestPromptTemplate(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.scenario = "integration_test_scenario" + cls.template_name = "test_prompt_template" + cls.version = "0.1.0" + cls.spec = PromptTemplateSpec(template=[PromptTemplate(role="system", content="You are a system under test.")]) + proxy_client = cast(GenAIHubProxyClient, get_proxy_client()) + cls.client = PromptTemplateClient(proxy_client=proxy_client) + + @classmethod + def tearDownClass(cls): + prompt_templates = cls.client.get_prompt_templates(scenario=cls.scenario, name=cls.template_name, + version=cls.version) + for template in prompt_templates.resources: + cls.client.delete_prompt_template_by_id(template.id) + + def setUp(self): + response = self.client.get_prompt_templates(scenario=self.scenario, name=self.template_name, + version=self.version) + if response.count > 0: + self.template_id = response.resources[0].id + else: + self.template_id = self.create_prompt_template(self.template_name) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def create_prompt_template(self, template_name: str) -> str: + spec = PromptTemplateSpec(template=[PromptTemplate(role="user", content="{{ ?user_input }}")]) + template_id = self.client.create_prompt_template(scenario=self.scenario, name=template_name, + version=self.version, prompt_template_spec=spec).id + return template_id + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_create_prompt_template(self): + response = self.client.create_prompt_template(scenario=self.scenario, name=self.template_name, + version=self.version, prompt_template_spec=self.spec) + self.assertIsInstance(response, PromptTemplatePostResponse) + + def test_get_prompt_templates(self): + response = self.client.get_prompt_templates(scenario=self.scenario, name=self.template_name, + version=self.version) + self.assertIsInstance(response, PromptTemplateListResponse) + self.assertGreater(response.count, 0, "No prompt templates found") + + def test_get_prompt_template_by_id(self): + response = self.client.get_prompt_template_by_id(self.template_id) + self.assertIsInstance(response, PromptTemplateGetResponse) + self.assertEqual(response.id, self.template_id) + + def test_get_prompt_template_by_id_not_found(self): + with self.assertRaises(AIAPIServerException): + self.client.get_prompt_template_by_id("non_existent_id") + + def test_get_prompt_template_history(self): + response = self.client.get_prompt_template_history(scenario=self.scenario, name=self.template_name, + version=self.version) + self.assertIsInstance(response, PromptTemplateListResponse) + + def test_delete_prompt_template_by_id(self): + template_id = self.client.create_prompt_template(scenario=self.scenario, name=self.template_name + '_delete', + version=self.version, prompt_template_spec=self.spec).id + response = self.client.delete_prompt_template_by_id(template_id) + self.assertIsInstance(response, PromptTemplateDeleteResponse) + + def test_import_prompt_template(self): + response = self.client.import_prompt_template(TEMPLATE_YAML.encode('utf-8')) + self.assertIsInstance(response, PromptTemplatePostResponse) + + # cleanup + response = self.client.delete_prompt_template_by_id(response.id) + self.assertIn("deleted successfully", response.message, "Template was not deleted") + + def test_export_prompt_template(self): + response = self.client.export_prompt_template(self.template_id) + self.assertEqual(type(response), bytes, "Exported template is not a byte stream") + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_fill_prompt_template_by_id(self): + template_id = self.create_prompt_template(template_name='test_substitute') + response = self.client.fill_prompt_template_by_id(template_id=template_id, input_params={"user_input": "Howdy"}) + self.assertIsInstance(response, PromptTemplateSubstitutionResponse) + + # cleanup + response = self.client.delete_prompt_template_by_id(template_id) + self.assertIn("deleted successfully", response.message, "Template was not deleted") + + def test_fill_prompt_template(self): + template_id = self.create_prompt_template(template_name='test_substitute') + response = self.client.fill_prompt_template(scenario=self.scenario, name='test_substitute', + version=self.version, metadata=True, + input_params={"user_input": "Howdy"},) + self.assertIsInstance(response, PromptTemplateSubstitutionResponse) + self.assertTrue(response.resource) + + # cleanup + response = self.client.delete_prompt_template_by_id(template_id) + self.assertIn("deleted successfully", response.message, "Template was not deleted") + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_create_prompt_template_with_response_format(self): + spec_with_response_format = PromptTemplateSpec(template=[ + PromptTemplate(role="user", content="Who is thw first man on the moon? Answer in json format.") + ], + response_format=ResponseFormatJsonObject()) + response = self.client.create_prompt_template(scenario=self.scenario, name=self.template_name, + version=self.version, + prompt_template_spec=spec_with_response_format) + self.assertIsInstance(response, PromptTemplatePostResponse) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_create_prompt_template_with_tools(self): + def modify_string(a: str) -> str: + """Modify a string by adding 'Hello' to the beginning.""" + return f'Hello {a}' + + tool = FunctionTool.from_function(modify_string) + spec_with_tools = PromptTemplateSpec(template=[ + PromptTemplate(role="user", content="Modify string using the function in tools. String: {{ ?string }} ") + ], + tools=[tool]) + response = self.client.create_prompt_template(scenario=self.scenario, name=self.template_name, + version=self.version, + prompt_template_spec=spec_with_tools) + self.assertIsInstance(response, PromptTemplatePostResponse) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_create_prompt_template_with_image_input(self): + data_url = ( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAE0lEQVR4nGP8z4APMOGVZRip0gBBLAETee26JgAAAABJRU5ErkJggg==' + ) + spec = PromptTemplateSpec( + template=[ + PromptTemplate(role="user", content=['What color is this image?', ImageItem(url=data_url)]) + ] + ) + + response = self.client.create_prompt_template(scenario=self.scenario, name=self.template_name, + version=self.version, + prompt_template_spec=spec) + self.assertIsInstance(response, PromptTemplatePostResponse) + + +class TestOrchestrationConfigClient(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.scenario = "integration_test_scenario" + cls.config_name = "test_config" + cls.version = "0.1.0" + cls.config_spec = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Hello, World!")]), + model=LLMModelDetails(name="gpt-4o-mini") + ) + ) + ) + proxy_client = cast(GenAIHubProxyClient, get_proxy_client()) + cls.client = OrchestrationConfigClient(proxy_client=proxy_client) + + @classmethod + def tearDownClass(cls): + configs = cls.client.get_orchestration_configs(scenario=cls.scenario, name=cls.config_name, + version=cls.version) + for config in configs.resources: + cls.client.delete_orchestration_config_by_id(config.id) + + def setUp(self): + response = self.client.get_orchestration_configs(scenario=self.scenario, name=self.config_name, + version=self.version) + if response.count > 0: + self.config = response.resources[0] + else: + self.config = self.create_orchestration_config(self.config_name) + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def create_orchestration_config(self, template_name: str) -> OrchestrationConfigPostResponse: + config = self.client.create_orchestration_config(scenario=self.scenario, + name=template_name, + version=self.version, + spec=self.config_spec) + return config + + @retry_on_429_or_503(max_retries=3, initial_delay=2.0, backoff_factor=2.0) + def test_create_orchestration_config(self): + response = self.client.create_orchestration_config(scenario=self.scenario, name=self.config_name, + version=self.version, spec=self.config_spec) + self.assertIsInstance(response, OrchestrationConfigPostResponse) + + def test_get_orchestration_configs_without_spec(self): + response = self.client.get_orchestration_configs(scenario=self.scenario, name=self.config_name, + version=self.version) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertGreater(response.count, 0) + self.assertIsNone(response.resources[0].spec) + + def test_get_orchestration_configs_with_spec(self): + response = self.client.get_orchestration_configs(scenario=self.scenario, name=self.config_name, + version=self.version, include_spec=True) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertGreater(response.count, 0) + self.assertIsNotNone(response.resources[0].spec) + + def test_get_orchestration_config_by_id(self): + response = self.client.get_orchestration_config_by_id(self.config.id) + self.assertIsInstance(response, OrchestrationConfigGetResponse) + self.assertEqual(response.id, self.config.id) + self.assertIsNotNone(response.spec) + self.assertEqual(response.spec.modules.prompt_templating.model.name, "gpt-4o-mini") + + def test_get_orchestration_config_by_id_not_found(self): + with self.assertRaises(AIAPIServerException): + self.client.get_orchestration_config_by_id("non_existent_id") + + def test_get_orchestration_config_history(self): + response = self.client.get_orchestration_config_history(scenario=self.scenario, name=self.config_name, + version=self.version) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertIsNone(response.resources[0].spec) + + def test_get_orchestration_config_history_with_spec(self): + response = self.client.get_orchestration_config_history(scenario=self.scenario, name=self.config_name, + version=self.version, include_spec=True, ) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertIsNotNone(response.resources[0].spec) + + def test_delete_orchestration_config_by_id(self): + config_id = self.client.create_orchestration_config(scenario=self.scenario, + name=self.config_name + '_delete', + version=self.version, + spec=self.config_spec).id + response = self.client.delete_orchestration_config_by_id(config_id) + self.assertIsInstance(response, OrchestrationConfigDeleteResponse) + self.assertIn("deleted successfully", response.message, "Config was not deleted") + + def test_import_orchestration_config(self): + response = self.client.import_orchestration_config(ORCHESTRATION_CONFIG_YAML.encode('utf-8')) + self.assertIsInstance(response, OrchestrationConfigPostResponse) + + # cleanup + response = self.client.delete_orchestration_config_by_id(response.id) + self.assertIn("deleted successfully", response.message, "Config was not deleted") + + def test_export_orchestration_config(self): + response = self.client.export_orchestration_config(self.config.id) + self.assertEqual(type(response), bytes, "Exported config is not a byte stream") \ No newline at end of file diff --git a/packages/gen/integration_tests/prompt_registry/test_data/test_orchestration_config.yaml b/packages/gen/integration_tests/prompt_registry/test_data/test_orchestration_config.yaml new file mode 100644 index 0000000..d9c6de7 --- /dev/null +++ b/packages/gen/integration_tests/prompt_registry/test_data/test_orchestration_config.yaml @@ -0,0 +1,15 @@ +name: simple +version: 0.0.1 +scenario: my-scenario +spec: + config: + modules: + - prompt_templating: + prompt: + template: + - role: user + content: "First man on the moon, answer in json" + response_format: + type: json_object + model: + name: gpt-4o \ No newline at end of file diff --git a/packages/gen/integration_tests/prompt_registry/test_data/test_prompt_template.yaml b/packages/gen/integration_tests/prompt_registry/test_data/test_prompt_template.yaml new file mode 100644 index 0000000..e454adb --- /dev/null +++ b/packages/gen/integration_tests/prompt_registry/test_data/test_prompt_template.yaml @@ -0,0 +1,9 @@ +name: simple +version: 0.0.1 +scenario: my-scenario +spec: + template: + - role: "system" + content: "{{ ?instruction }}" + - role: "user" + content: "Some more {{ ?user_input }}" \ No newline at end of file diff --git a/packages/gen/integration_tests/setup_aicore.py b/packages/gen/integration_tests/setup_aicore.py new file mode 100644 index 0000000..e522ff2 --- /dev/null +++ b/packages/gen/integration_tests/setup_aicore.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +import time +from dataclasses import dataclass, field +from functools import lru_cache +from typing import Any, ClassVar, Dict, List, Optional + +from ai_api_client_sdk.models.parameter_binding import ParameterBinding +from ai_api_client_sdk.models.status import Status +from ai_core_sdk.ai_core_v2_client import AICoreV2Client + +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context + +FOUNDATION_MODEL_SCENARIO = 'foundation-models' + + +@dataclass +class FoundationalModelExecutable: + _instances: ClassVar[Dict[str, 'FoundationalModelExecutable']] = {} + _model_to_scenario: ClassVar[Dict[str, 'FoundationalModelExecutable']] = {} + _discovered: ClassVar[bool] = False + + id_: str + model_version_default: Optional[str] = None + models: List[str] = field(default_factory=list) + + def __post_init__(self): + # Assign a unique ID and store the instance + FoundationalModelExecutable._instances[self.id_] = self + for model in self.models: + FoundationalModelExecutable._model_to_scenario[model] = self + + @classmethod + def get_instance(cls, scenario_id: str) -> FoundationalModelExecutable: + # Retrieve an instance by its ID + return cls._instances.get(scenario_id) + + @classmethod + def get_executable(cls, model_name: str) -> FoundationalModelExecutable: + # Retrieve an instance by its ID + return cls._model_to_scenario.get(model_name) + + @classmethod + def discover(cls, client: AICoreV2Client, scenario_id: str = FOUNDATION_MODEL_SCENARIO, force: bool = False): + if cls._discovered and not force: + return + execs = client.executable.query(scenario_id=scenario_id) + for exe in execs.resources: + models = None + model_version_default = None + for param in exe.parameters: + if param.name == 'modelName': + models = [] + if param.description is not None: + models = [m.strip() for m in param.description.partition(':')[-1].split(',')] + if param.default and param.default not in models: + models.append(param.default) + if param.name == 'modelVersion': + model_version_default = param.default + cls( + id_=exe.id, + model_version_default=model_version_default, + models=models, + ) + cls._discovered = True + + +def find_existing_config(client, executable_id, params, scenario_id=FOUNDATION_MODEL_SCENARIO): + parameter_values_new = {p.key: p.value for p in params} + configs = client.configuration.query( + scenario_id=scenario_id) + for config in configs.resources: + if config.executable_id != executable_id: + continue + parameter_values = {p.key: p.value for p in config.parameter_bindings} + parameter_values['name'] = config.name + if parameter_values == parameter_values_new: + return config + + +def get_deployments(ai_core_client, status=(Status.PENDING, Status.RUNNING, Status.UNKNOWN), **kwargs): + if isinstance(status, Status): + status = [status] + deployments = [] + for status_ in status: + deployments.extend( + ai_core_client.deployment.query( + **{**kwargs, 'status': status_} + ).resources) + return deployments + + +def deploy(ai_core_client: AICoreV2Client, + model_name: str, + model_version: Optional[str] = None, + config_name: Optional[str] = None, + executable_id: Optional[str] = None, + scenario_id: str = FOUNDATION_MODEL_SCENARIO, + force: bool = False): + FoundationalModelExecutable.discover(ai_core_client, scenario_id=scenario_id) + + config_name = model_name + params = [ParameterBinding( + key='modelName', + value=model_name, + )] + if model_version: + config_name = f'{model_name}-{model_version}' + params.append(ParameterBinding( + key='modelVersion', + value=model_version, + )) + + params.append(ParameterBinding( + key='name', + value=config_name, + )) + + executable = find_executable(model_name=model_name, executable_id=executable_id) + config = find_existing_config(ai_core_client, executable.id_, params, scenario_id=scenario_id) + if config is None: + config_name = config_name or f'{model_name}-{model_version or "default"}' + config = ai_core_client.configuration.create( + scenario_id=FOUNDATION_MODEL_SCENARIO, + executable_id=executable.id_, + name=config_name, + parameter_bindings=params, + ) + deployments = get_deployments(ai_core_client, scenario_id=scenario_id, configuration_id=config.id) + if len(deployments) > 0 and not force: + return False, deployments[0], config + return True, ai_core_client.deployment.create(configuration_id=config.id), config + + +def find_executable(model_name: str, executable_id: Optional[str] = None): + if executable_id is None: + executable = FoundationalModelExecutable.get_executable(model_name) + else: + try: + executable = FoundationalModelExecutable.get_instance(executable_id) + except KeyError: + executable = FoundationalModelExecutable(id_=executable_id, models=[model_name]) + + if executable is None: + raise ValueError(f"Executable not found for model '{model_name}'.") + + return executable + + +def get_bedrock_models(): + """Return list of bedrock models (Amazon and Anthropic models available via Bedrock)""" + return [ + ("amazon--titan-embed-text", "latest"), + ("amazon--nova-micro", "latest"), + ("amazon--nova-premier", "latest"), + ("anthropic--claude-4.5-haiku", "latest"), + ("anthropic--claude-4.5-sonnet", "latest") + ] + + +def get_standard_models(): + """Return list of standard models (non-bedrock models)""" + return [ + ("gpt-4o-mini", "latest"), + ("gpt-5", "latest"), + ("gpt-5-mini", "latest"), + ("gpt-5-nano", "latest"), + ("o3-mini", "latest"), + ("o4-mini", "latest"), + ("mistralai--mistral-small-instruct", "latest"), + ("nvidia--llama-3.2-nv-embedqa-1b", "latest"), + ("text-embedding-3-small", "latest"), + ("gemini-2.5-flash-lite", "latest"), + ("gemini-embedding", "latest"), + ("sonar", "latest"), + ("sonar-deep-research", "latest"), + ("cohere--command-a-reasoning", "latest"), + ("sap-rpt-1-small","latest"), + ] + + +def _setup_models(client: AICoreV2Client, models: List[tuple], max_wait_seconds=1200): + """Internal function to setup a specific list of models""" + running_deployments = [] + pending_deployments = [] + deployment_id_to_model = {} + + for model, model_version in models: + try: + newly_deployed, deployment, _ = deploy(ai_core_client=client, model_name=model, model_version=model_version) + if newly_deployed or deployment.status in (Status.PENDING, Status.UNKNOWN): + pending_deployments.append(deployment) + else: + running_deployments.append(deployment) + deployment_id_to_model[deployment.id] = model + except Exception as e: + print(f"Error for virtual deployment of model {model}-{model_version}: {e}") + continue # Skip model if it is not available + + if pending_deployments: + check_pending_deployments(client, max_wait_seconds, pending_deployments, running_deployments) + + return {deployment_id_to_model[dep.id]: dep for dep in running_deployments} + + +def check_pending_deployments(client: AICoreV2Client, max_wait_seconds: int, pending_deployments: list[Any], + running_deployments: list[Any]): + checked_deployments = [] + start = time.time() + while pending_deployments or checked_deployments: + if time.time() - start > max_wait_seconds: + raise TimeoutError('Timeout waiting for deployments to start.') + dep = pending_deployments.pop(0) + dep = client.deployment.get(dep.id) + if dep.status == Status.RUNNING: + running_deployments.append(dep) + else: + checked_deployments.append(dep) + if len(pending_deployments) == 0: + pending_deployments = checked_deployments + checked_deployments = [] + time.sleep(10) + + +@lru_cache +def setup_bedrock_models(client: AICoreV2Client, max_wait_seconds=1200): + """Setup only bedrock models (Amazon and Anthropic models)""" + bedrock_models = get_bedrock_models() + return _setup_models(client, bedrock_models, max_wait_seconds) + + +@lru_cache +def setup_standard_models(client: AICoreV2Client, max_wait_seconds=1200): + """Setup only standard (non-bedrock) models""" + standard_models = get_standard_models() + return _setup_models(client, standard_models, max_wait_seconds) + + +@lru_cache +def setup_aicore_instance(client: AICoreV2Client, max_wait_seconds=1200): + """Setup all models (for backward compatibility)""" + all_models = get_bedrock_models() + get_standard_models() + return _setup_models(client, all_models, max_wait_seconds) + + +class TestCaseAICoreSetupMixin: + """Base mixin for AI Core setup - sets up all models""" + @classmethod + def setUpClass(cls): # noqa: N802 - unittest framework method name + with proxy_version_context('gen-ai-hub'): + cls.proxy_client = get_proxy_client() + cls.aicore_deployments = setup_aicore_instance(cls.proxy_client.ai_core_client) + + +class TestCaseBedrockSetupMixin: + """Mixin specifically for bedrock tests - sets up only bedrock models""" + @classmethod + def setUpClass(cls): # noqa: N802 - unittest framework method name + with proxy_version_context('gen-ai-hub'): + cls.proxy_client = get_proxy_client() + cls.aicore_deployments = setup_bedrock_models(cls.proxy_client.ai_core_client) + + +class TestCaseStandardSetupMixin: + """Mixin specifically for standard tests - sets up only standard models""" + @classmethod + def setUpClass(cls): # noqa: N802 - unittest framework method name + with proxy_version_context('gen-ai-hub'): + cls.proxy_client = get_proxy_client() + cls.aicore_deployments = setup_standard_models(cls.proxy_client.ai_core_client) diff --git a/packages/gen/integration_tests/test_helpers.py b/packages/gen/integration_tests/test_helpers.py new file mode 100644 index 0000000..4dd19b8 --- /dev/null +++ b/packages/gen/integration_tests/test_helpers.py @@ -0,0 +1,133 @@ +"""Helper utilities for making integration tests more reliable.""" +import time +import inspect +from functools import wraps +from typing import Callable, TypeVar, Any +import logging +import pytest + +logger = logging.getLogger(__name__) + +T = TypeVar('T') + + +def retry_on_429_or_503(max_retries: int = 3, initial_delay: float = 2.0, backoff_factor: float = 2.0, + skip_on_failure: bool = False): + """ + Decorator to retry a function when it encounters rate limiting (429) or temporary service errors (503). + + :param max_retries: Maximum number of retry attempts + :param initial_delay: Initial delay in seconds before first retry + :param backoff_factor: Multiplier for delay between retries (exponential backoff) + :param skip_on_failure: If True, skip the test instead of failing when rate limit (429) retries are exhausted + """ + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + delay = initial_delay + last_exception = None + was_rate_limited = False + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + last_exception = e + error_message = str(e).lower() + + # Check if it's a retryable error + is_rate_limit = '429' in str(e) or 'too many requests' in error_message + is_service_unavailable = '503' in str(e) or 'service unavailable' in error_message + is_gateway_timeout = '504' in str(e) or 'gateway' in error_message + + if is_rate_limit: + was_rate_limited = True + + if not (is_rate_limit or is_service_unavailable or is_gateway_timeout): + # Not a retryable error, raise immediately + raise + + if attempt < max_retries: + logger.warning( + f"Attempt {attempt + 1}/{max_retries + 1} failed with retryable error: {e}. " + f"Retrying in {delay:.1f}s..." + ) + time.sleep(delay) + delay *= backoff_factor + else: + logger.error(f"All {max_retries + 1} attempts failed. Last error: {e}") + + # Skip test if configured and it was a rate limit issue + if skip_on_failure and was_rate_limited: + pytest.skip(f"Test skipped after {max_retries + 1} failed attempts due to rate limiting: " + f"{last_exception}") + + raise last_exception + + return wrapper + return decorator + +def retry_on_429_or_503_class(max_retries: int = 3, initial_delay: float = 2.0, backoff_factor: float = 2.0, + skip_on_failure: bool = False): + """ + Class decorator to apply retry_on_429_or_503 to all test methods in a class. + :param max_retries: Maximum number of retry attempts + :param initial_delay: Initial delay in seconds before first retry + :param backoff_factor: Multiplier for delay between retries (exponential backoff) + :param skip_on_failure: If True, skip the test instead of failing when rate limit (429) retries are exhausted + """ + def class_decorator(cls): + for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith('test'): + decorated = retry_on_429_or_503( + max_retries=max_retries, + initial_delay=initial_delay, + backoff_factor=backoff_factor, + skip_on_failure=skip_on_failure + )(method) + setattr(cls, name, decorated) + return cls + return class_decorator + +def with_retry_on_missing_resource(max_retries: int = 3, delay: float = 2.0): + """ + Decorator to retry a function when it encounters a missing resource error. + Useful for handling race conditions where resources haven't fully propagated yet. + + :param max_retries: Maximum number of retry attempts + :param delay: Delay in seconds between retries + """ + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> T: + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + last_exception = e + error_message = str(e).lower() + + # Check if it's a missing resource error + is_not_found = ('not found' in error_message or + '404' in str(e) or + 'collection' in error_message and 'not found' in error_message) + + if not is_not_found: + # Not a missing resource error, raise immediately + raise + + if attempt < max_retries: + logger.warning( + f"Attempt {attempt + 1}/{max_retries + 1} failed with missing resource: {e}. " + f"Waiting {delay}s for resource to be available..." + ) + time.sleep(delay) + else: + logger.error(f"Resource still not found after {max_retries + 1} attempts. Last error: {e}") + + raise last_exception + + return wrapper + return decorator diff --git a/packages/gen/local_development_evaluations.MD b/packages/gen/local_development_evaluations.MD new file mode 100644 index 0000000..10a556d --- /dev/null +++ b/packages/gen/local_development_evaluations.MD @@ -0,0 +1,147 @@ +# Local Development Guide for Evaluations + +This guide provides step-by-step instructions for developing and testing the Evaluations SDK locally. + +## Prerequisites + +- Python 3.8 or higher +- pip package manager +- Virtual environment tool (venv or conda) + +## Setup Steps + +### 1. Create and Activate Virtual Environment + +**Using venv:** +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +**Using conda:** +```bash +conda create -n gen-ai-hub python=3.10 +conda activate gen-ai-hub +``` + +### 2. Install Dependencies + +Install all required dependencies from the root directory: +```bash +pip install -r requirements.txt +``` + +### 3. Install SDK in Editable Mode + +To ensure SDK changes are immediately reflected without reinstalling: +```bash +pip install -e . +``` + +> **Note**: Run this command every time you make changes to the SDK code. The `-e` flag installs the package in "editable" mode, linking to your local source code. + +## Testing Local Changes + +### 4. Test with Client Script + +For quick testing, you can hardcode credentials and create a test client: + +1. Open `gen_ai_hub/evaluations/client.py` +2. Add your test code in the `if __name__ == "__main__":` section +3. Run from the root directory: + ```bash + python gen_ai_hub/evaluations/client.py + ``` + +### 5. Run Integration Tests + +To run integration tests with proper environment setup: + +```bash +# Set up environment variables +source integration_tests/evaluations/set_env.sh + +# Run single execution flow test +pytest integration_tests/evaluations/test_single_execution_flow.py -v + +# Run multiple execution flow test +pytest integration_tests/evaluations/test_multiple_execution_flow.py -v + +# Run all integration tests +pytest integration_tests/evaluations/ -v +``` + +## Building Distribution Files + +### 6. Create Wheel File for Testing + +To create a distributable `.whl` file for testing in notebooks or other environments: + +1. **Install build tools** (if not already installed): + ```bash + pip install build + ``` + +2. **Build the distribution**: + ```bash + python -m build + ``` + + This creates two files in the `dist/` folder: + - `*.tar.gz` (source distribution) + - `*.whl` (wheel distribution) + +3. **Use the wheel file**: + ```bash + pip install dist/generative_ai_hub_sdk-*.whl + ``` + +### 7. Change SDK Version + +To release a custom version (e.g., changing from 5.7.x to a different version): + +1. Update version in `setup.py` or `pyproject.toml` +2. Update version in `gen_ai_hub/__init__.py` +3. Update version in any other relevant files +4. Rebuild the distribution: + ```bash + python -m build + ``` + +## Development Workflow Summary + +```bash +# 1. Make code changes +vim gen_ai_hub/evaluations/client.py + +# 2. Reinstall in editable mode +pip install -e . + +# 3. Test your changes +python gen_ai_hub/evaluations/client.py +# OR +pytest integration_tests/evaluations/ + +# 4. Build distribution (optional) +python -m build +``` + +## Troubleshooting + +### Changes Not Reflected +- Make sure you ran `pip install -e .` after making changes +- Restart your Python interpreter/kernel + +### Import Errors +- Verify all dependencies are installed: `pip install -r requirements.txt` +- Check that you're using the correct virtual environment + +### Integration Test Failures +- Ensure environment variables are set (source `set_env.sh`) +- Verify your AI Core credentials have the necessary permissions +- Check that the base URL includes the `/v2` suffix + +## Additional Resources + +- [Integration Tests README](integration_tests/evaluations/README.md) +- [SDK Documentation](docs/SDK%20doc.md) \ No newline at end of file diff --git a/packages/gen/pyproject.toml b/packages/gen/pyproject.toml new file mode 100644 index 0000000..4dc9d9a --- /dev/null +++ b/packages/gen/pyproject.toml @@ -0,0 +1,95 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "sap-ai-sdk-gen" +version = "7.2.0" +description = "SAP Cloud SDK for AI (Python): generative AI SDK" +readme = "PYPIDESCRIPTION.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [{ name = "SAP SE" }] +keywords = ["SAP AI Core", "SAP generative AI SDK", "SAP Generative AI Hub"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows :: Windows 10", + "Operating System :: Microsoft :: Windows :: Windows 7", + "Operating System :: Microsoft :: Windows :: Windows 8", + "Operating System :: Microsoft :: Windows :: Windows 8.1", + "Operating System :: Microsoft :: Windows :: Windows Server 2008", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.9" +dependencies = [ + "httpx>=0.27.0", + "h11>=0.16.0", + "dacite>=1.8.1", + "click>=8.1.7", + "overloading==0.5.0", + "packaging>=23.2", + "sap-ai-sdk-core>=3.3.0", + "pydantic~=2.12", + "openai>=1.66.0", + "langcodes~=3.5.1", + "pandas>=2.2.0", + "langchain~=1.2.14", + "langchain-classic~=1.0.0", + "langchain-community~=0.4.1", + "langchain-openai~=1.2.0", +] + +[project.optional-dependencies] +google = ["google-genai~=1.73.1", "langchain-google-genai~=4.2.0"] +amazon = ["boto3>=1.40.61", "aiobotocore>=3.2.0", "langchain-aws~=1.4.0"] +all = ["sap-ai-sdk-gen[google,amazon]"] + +[dependency-groups] +dev = [ + "pytest==9.0.3", + "pytest-cov==7.1.0", + "pytest-asyncio==1.3.0", + "pylint==4.0.5", + "requests-mock==1.12.1", + "respx==0.23.1", + "parameterized==0.9.0", + "pillow==12.2.0", + "sphinx<9.0.0", + "myst_nb", + "sphinxawesome-theme", + "pytest-dotenv>=0.5.2", +] + +[tool.pytest.ini_options] +env_files = [".env"] + +[project.urls] +Homepage = "https://www.sap.com/" +Download = "https://pypi.python.org/pypi/sap-ai-sdk-gen" + +[tool.uv.sources] +sap-ai-sdk-core = { workspace = true } + +[tool.setuptools.packages.find] +exclude = ["*test*"] + +[tool.commitizen] +name = "cz_customize" +tag_format = "gen-v${version}" +ignored_tag_formats = ["*-v${version}"] +version_provider = "pep621" +changelog_file = "RELEASE_NOTES.md" + +[tool.commitizen.customize] +bump_pattern = '^(feat|fix)\(gen\)' +changelog_pattern = '^(feat|fix)\(gen\)' diff --git a/packages/gen/scripts/prompt_template_registry_cleanup.py b/packages/gen/scripts/prompt_template_registry_cleanup.py new file mode 100644 index 0000000..c549f75 --- /dev/null +++ b/packages/gen/scripts/prompt_template_registry_cleanup.py @@ -0,0 +1,91 @@ +import argparse + +from gen_ai_hub.prompt_registry.client import PromptTemplateClient +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateGetResponse + + +### ENVIRONMENT VARIABLES --- SET IF NECESSARY ### +# os.environ["AICORE_AUTH_URL"] = "https://mlfwdftest.authentication.sap.hana.ondemand.com/oauth/token" +# os.environ["AICORE_BASE_URL"] = "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2" +# os.environ["AICORE_RESOURCE_GROUP"] = "default" +# os.environ["AICORE_CLIENT_ID"] = "" +# os.environ["AICORE_CLIENT_SECRET"] = "" +### + +def delete_prompt_template(prompt_template:PromptTemplateGetResponse, scenario:str, dry_run:bool): + """ + Delete all versions of a given prompt template for a specific scenario. + + Args: + prompt_template (PromptTemplateGetResponse): The prompt template object to delete. + scenario (str): The scenario associated with the template. + dry_run (bool): If True, perform a dry run without actual deletion. + """ + template_history = client.get_prompt_template_history(scenario=scenario, + name=prompt_template.name, + version=prompt_template.version) + print(f"Deleting test prompt template versions for template '{prompt_template.name}'." + f" Found {len(template_history.resources)} versions.") + for history_version in template_history.resources: + delete_history_version(history_version, client, dry_run) + +def delete_history_version(history_version:PromptTemplateGetResponse, + prompt_template_client:PromptTemplateClient, + dry_run:bool): + """ + Delete a specific version of a prompt template if it is not the head version. + + Args: + history_version (PromptTemplateGetResponse): The version of the prompt template to delete. + prompt_template_client (PromptTemplateClient): The client used to interact with the prompt template API. + dry_run (bool): If True, perform a dry run without actual deletion. + """ + version = history_version.version + name = history_version.name + scenario = history_version.scenario + if not history_version.is_version_head: + if not dry_run: + prompt_template_client.delete_prompt_template_by_id(history_version.id) + prefix = "DRY RUN:" if dry_run else "" + print(f"{prefix} Deleted prompt template version: " + f"name={name}, " + f"version={version}, " + f"scenario={scenario}, " + f"id={history_version.id}") + else: + print(f"Skipping deletion of the head version: " + f"name={name}, " + f"version={version}, " + f"scenario={scenario}, " + f"id={history_version.id}") + +if __name__ == "__main__": + """ + Main script to clean up prompt templates for a specified scenario. + + This script retrieves all prompt templates for the given scenario and deletes + their versions, except for the head version. + + Command-line Arguments: + --scenario_name (str): The scenario name to process. Default is "test_scenario". + --delete (str): "True" or "False" to indicate if templates should be deleted. Default is "false". + """ + parser = argparse.ArgumentParser(description="Cleanup script for prompt templates.") + parser.add_argument("--scenario_name", + required=False, + help="The scenario name to process.", + default="test_scenario") + parser.add_argument("--delete", + required=False, + help="True or False to indicate if templates should be deleted", + default="false") + args = parser.parse_args() + scenario_name = args.scenario_name + dry_run = args.delete.lower() == "false" + + client = PromptTemplateClient() + templates = client.get_prompt_templates(scenario=scenario_name, name=None, version=None) + print(f"Deleting test prompt templates for scenario '{scenario_name}'." + f" Found {len(templates.resources)} templates.") + for template in templates.resources: + delete_prompt_template(template, scenario_name, dry_run) diff --git a/packages/gen/sonar-project.properties b/packages/gen/sonar-project.properties new file mode 100644 index 0000000..b3bc99a --- /dev/null +++ b/packages/gen/sonar-project.properties @@ -0,0 +1,20 @@ +sonar.projectKey=generative-ai-hub-sdk +sonar.projectName=generative-ai-hub-sdk +sonar.projectVersion=7.0.0 +sonar.python.version=3.10 +sonar.sources=./gen_ai_hub +sonar.exclusions=scripts/**,tests/**,tests/**,integration_tests/**,integration_tests/** +sonar.cpd.exclusions=gen_ai_hub/proxy/native/openai/clients.py,**/orchestration_v2/sse_client.py,**/orchestration_v2/service.py,**/orchestration_v2/models/tools.py,**/orchestration_v2/models/data_masking.py,**/orchestration_v2/models/multimodal_items.py,**/orchestration_v2/__init__.py,**/**/orchestration_v2/models/__init__.py, **/**/document_grounding/__init__.py, **/**/document_grounding/models/__init__.py +sonar.dynamicAnalysis=reuseReports +sonar.core.codeCoveragePlugin=cobertura +sonar.python.coverage.reportPaths=coverage.xml +sonar.python.xunit.reportPath=unit_tests.xml +sonar.python.pylint.reportPath=pylint.log +sonar.qualitygate.wait=true + +# project metadata +sonar.links.homepage=https://www.sap.com +sonar.links.ci=https://jenkins.ml.only.sap/job/AI-Foundation/job/generative-ai-hub-sdk/ +sonar.pullrequest.github.repository=AI/generative-ai-hub-sdk +sonar.links.issue=https://sapjira.wdf.sap.corp/projects/AIWDF +sonar.links.scm=https://github.wdf.sap.corp/AI/generative-ai-hub-sdk diff --git a/packages/gen/tests/__init__.py b/packages/gen/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/batch_service/__init__.py b/packages/gen/tests/batch_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/batch_service/test_batch_service.py b/packages/gen/tests/batch_service/test_batch_service.py new file mode 100644 index 0000000..520531f --- /dev/null +++ b/packages/gen/tests/batch_service/test_batch_service.py @@ -0,0 +1,216 @@ +""" +Unit tests for the BatchService client — synchronous methods. +""" + +import unittest +from unittest.mock import patch + +import httpx +from httpx import Response + +from gen_ai_hub.batch_service.exceptions import BatchServiceError +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, + BatchDeleteResponse, +) +from gen_ai_hub.batch_service.service import BatchService +from tests.mock import ( + BATCH_ID, + BATCH_CREATE_RESPONSE as CREATE_RESPONSE, + batch_create_mocker, + batch_list_mocker, + batch_get_mocker, + batch_status_mocker, + batch_cancel_mocker, + batch_delete_mocker, + batch_not_found_mocker, + batch_create_error_mocker, + get_mocked_ai_core_client +) + + +class TestBatchService(unittest.TestCase): + + def setUp(self): + self.proxy_client = get_mocked_ai_core_client(client_id='test') + self.client = BatchService(proxy_client=self.proxy_client) + + def test_create_returns_create_response(self): + with batch_create_mocker(): + resp = self.client.create( + type="llm-native", + input_uri="ai://store/input.jsonl", + output_uri="ai://store/output/", + provider="azure-openai", + model="gpt-4.1-mini", + ) + self.assertIsInstance(resp, BatchCreateResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertEqual(resp.status, "PENDING") + + def test_create_sends_correct_payload(self): + captured = {} + + def capture_post(url, **kwargs): + captured["json"] = kwargs.get("json") + return Response(202, json=CREATE_RESPONSE) + + with patch.object(self.client.client, "post", side_effect=capture_post): + self.client.create( + type="llm-native", + input_uri="ai://store/input.jsonl", + output_uri="ai://store/output/", + provider="azure-openai", + model="gpt-4.1-mini", + ) + + payload = captured["json"] + self.assertEqual(payload["type"], "llm-native") + self.assertEqual(payload["input"]["uri"], "ai://store/input.jsonl") + self.assertEqual(payload["output"]["uri"], "ai://store/output/") + self.assertEqual(payload["spec"]["provider"], "azure-openai") + self.assertEqual(payload["spec"]["model"], "gpt-4.1-mini") + + def test_with_resource_group_header(self): + test_resource_group = 'test-resource-group' + client = BatchService(proxy_client=self.proxy_client, resource_group=test_resource_group) + captured_headers = {} + + def capture_post(url, **kwargs): + captured_headers.update(kwargs.get("headers", {})) + return Response(202, json=CREATE_RESPONSE) + + with patch.object(client.client, "post", side_effect=capture_post): + client.create( + type="llm-native", + input_uri="ai://x", + output_uri="ai://y", + provider="p", + model="m", + ) + + self.assertEqual(captured_headers.get("AI-Resource-Group"), test_resource_group) + + def test_create_raises_batch_service_error_on_400(self): + + with batch_create_error_mocker(): + with self.assertRaises(BatchServiceError) as ctx: + self.client.create( + type="llm-native", + input_uri="ai://bad.txt", + output_uri="ai://y", + provider="p", + model="m", + ) + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("not found", ctx.exception.message.lower()) + + def test_list_returns_list_response(self): + + with batch_list_mocker(): + resp = self.client.list() + self.assertIsInstance(resp, BatchListResponse) + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + self.assertEqual(resp.resources[0].id, BATCH_ID) + + def test_get_returns_detail_response(self): + + with batch_get_mocker(): + resp = self.client.get(BATCH_ID) + self.assertIsInstance(resp, BatchDetailResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertEqual(resp.status.current_status, "COMPLETED") + self.assertEqual(resp.input.uri, "ai://my-store/input/batch-input.jsonl") + + def test_get_raises_on_404(self): + + with batch_not_found_mocker(): + with self.assertRaises(BatchServiceError) as ctx: + self.client.get(BATCH_ID) + self.assertEqual(ctx.exception.status_code, 404) + self.assertIn("not found", ctx.exception.message.lower()) + + def test_get_status_returns_status_response(self): + + with batch_status_mocker(): + resp = self.client.get_status(BATCH_ID) + self.assertIsInstance(resp, BatchStatusResponse) + self.assertEqual(resp.current_status, "RUNNING") + self.assertEqual(resp.target_status, "COMPLETED") + self.assertIsNone(resp.message) + + def test_cancel_returns_cancel_response(self): + + with batch_cancel_mocker(): + resp = self.client.cancel(BATCH_ID) + self.assertIsInstance(resp, BatchCancelResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertIn("cancellation", resp.message.lower()) + + def test_delete_returns_delete_response(self): + + with batch_delete_mocker(): + resp = self.client.delete(BATCH_ID) + self.assertIsInstance(resp, BatchDeleteResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertIn("deleted", resp.message.lower()) + + def test_timeout_priority_no_timeout_set(self): + + captured = {} + + def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return Response(202, json=CREATE_RESPONSE) + + with patch.object(self.client.client, "post", side_effect=capture_post): + self.client.create(type="llm-native", input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + + self.assertEqual(captured["timeout"], httpx.USE_CLIENT_DEFAULT) + + def test_timeout_priority_service_default(self): + client = BatchService(proxy_client=self.proxy_client, timeout=99.0) + captured = {} + + def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return Response(202, json=CREATE_RESPONSE) + + with patch.object(client.client, "post", side_effect=capture_post): + client.create(type="llm-native", input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + + self.assertEqual(captured["timeout"], 99.0) + + def test_timeout_priority_per_request_overrides_default(self): + client = BatchService(proxy_client=self.proxy_client, timeout=99.0) + captured = {} + + def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return Response(202, json=CREATE_RESPONSE) + + with patch.object(client.client, "post", side_effect=capture_post): + client.create(type="llm-native", input_uri="ai://x", output_uri="ai://y", provider="p", model="m", timeout=77.0) + + self.assertEqual(captured["timeout"], 77.0) + + def test_http_client_is_reused_across_requests(self): + + original_client = self.client.client + with batch_create_mocker(): + self.client.create(type="llm-native", input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + with batch_list_mocker(): + self.client.list() + self.assertIs(self.client.client, original_client) + self.assertFalse(original_client.is_closed) + + def test_close_http_connection(self): + + client_ref = self.client.client + self.client.close_http_connection() + self.assertTrue(client_ref.is_closed) diff --git a/packages/gen/tests/batch_service/test_batch_service_async.py b/packages/gen/tests/batch_service/test_batch_service_async.py new file mode 100644 index 0000000..e1862c7 --- /dev/null +++ b/packages/gen/tests/batch_service/test_batch_service_async.py @@ -0,0 +1,135 @@ +""" +Unit tests for the BatchService client — async methods. +""" + +import unittest +from unittest.mock import AsyncMock, patch + +import httpx + +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, + BatchDeleteResponse, +) +from gen_ai_hub.batch_service.service import BatchService +from tests.mock import ( + BATCH_ID, + BATCH_CREATE_RESPONSE as CREATE_RESPONSE, + batch_create_mocker_async, + batch_list_mocker_async, + batch_get_mocker_async, + batch_status_mocker_async, + batch_cancel_mocker_async, + batch_delete_mocker_async, + get_mocked_ai_core_client +) + + +class TestBatchServiceAsync(unittest.IsolatedAsyncioTestCase): + + def setUp(self): + self.proxy_client = get_mocked_ai_core_client(client_id='test') + self.client = BatchService(proxy_client=self.proxy_client) + + async def test_acreate_returns_create_response(self): + async with batch_create_mocker_async(): + resp = await self.client.acreate( + input_uri="ai://store/input.jsonl", + output_uri="ai://store/output/", + provider="azure-openai", + model="gpt-4.1-mini", + ) + self.assertIsInstance(resp, BatchCreateResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertEqual(resp.status, "PENDING") + + async def test_alist_returns_list_response(self): + async with batch_list_mocker_async(): + resp = await self.client.alist() + self.assertIsInstance(resp, BatchListResponse) + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + + async def test_aget_returns_detail_response(self): + async with batch_get_mocker_async(): + resp = await self.client.aget(BATCH_ID) + self.assertIsInstance(resp, BatchDetailResponse) + self.assertEqual(resp.id, BATCH_ID) + self.assertEqual(resp.status.current_status, "COMPLETED") + + async def test_aget_status_returns_status_response(self): + async with batch_status_mocker_async(): + resp = await self.client.aget_status(BATCH_ID) + self.assertIsInstance(resp, BatchStatusResponse) + self.assertEqual(resp.current_status, "RUNNING") + self.assertIsNone(resp.message) + + async def test_acancel_returns_cancel_response(self): + async with batch_cancel_mocker_async(): + resp = await self.client.acancel(BATCH_ID) + self.assertIsInstance(resp, BatchCancelResponse) + self.assertEqual(resp.id, BATCH_ID) + + async def test_adelete_returns_delete_response(self): + async with batch_delete_mocker_async(): + resp = await self.client.adelete(BATCH_ID) + self.assertIsInstance(resp, BatchDeleteResponse) + self.assertEqual(resp.id, BATCH_ID) + + async def test_async_timeout_no_timeout_set(self): + captured = {} + + async def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return httpx.Response(202, json=CREATE_RESPONSE) + + with patch.object(self.client.async_client, "post", new=AsyncMock(side_effect=capture_post)): + await self.client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + + self.assertEqual(captured["timeout"], httpx.USE_CLIENT_DEFAULT) + + async def test_async_timeout_service_default(self): + client = BatchService(proxy_client=self.proxy_client, timeout=55.0) + captured = {} + + async def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return httpx.Response(202, json=CREATE_RESPONSE) + + with patch.object(client.async_client, "post", new=AsyncMock(side_effect=capture_post)): + await client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + + self.assertEqual(captured["timeout"], 55.0) + + async def test_async_timeout_per_request_overrides_default(self): + client = BatchService(proxy_client=self.proxy_client, timeout=55.0) + captured = {} + + async def capture_post(url, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return httpx.Response(202, json=CREATE_RESPONSE) + + with patch.object(client.async_client, "post", new=AsyncMock(side_effect=capture_post)): + await client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m", timeout=33.0) + + self.assertEqual(captured["timeout"], 33.0) + + async def test_async_client_is_reused(self): + + original_async_client = self.client.async_client + async with batch_create_mocker_async(): + await self.client.acreate(input_uri="ai://x", output_uri="ai://y", provider="p", model="m") + async with batch_list_mocker_async(): + await self.client.alist() + self.assertIs(self.client.async_client, original_async_client) + self.assertFalse(original_async_client.is_closed) + + async def test_aclose_http_connection(self): + + async_client_ref = self.client.async_client + await self.client.aclose_http_connection() + self.assertTrue(async_client_ref.is_closed) diff --git a/packages/gen/tests/batch_service/test_batch_service_models.py b/packages/gen/tests/batch_service/test_batch_service_models.py new file mode 100644 index 0000000..a7151d5 --- /dev/null +++ b/packages/gen/tests/batch_service/test_batch_service_models.py @@ -0,0 +1,167 @@ +""" +Unit tests for batch service model serialization and validation. +""" + +import unittest + +from pydantic import ValidationError + +from gen_ai_hub.batch_service.models.request import BatchCreateRequest, BatchInput, BatchOutput, BatchSpec +from gen_ai_hub.batch_service.models.response import ( + BatchCreateResponse, + BatchSummary, + BatchListResponse, + BatchDetailResponse, + BatchStatusResponse, + BatchCancelResponse, + BatchDeleteResponse, + ErrorResponse, +) + + +class TestBatchCreateRequest(unittest.TestCase): + + def _make_request(self, **overrides): + defaults = dict( + type="llm-native", + input=BatchInput(uri="ai://store/input.jsonl"), + output=BatchOutput(uri="ai://store/output/"), + spec=BatchSpec(provider="azure-openai", model="gpt-4.1-mini"), + ) + defaults.update(overrides) + return BatchCreateRequest(**defaults) + + def test_serialization_uses_snake_case_keys(self): + req = self._make_request() + payload = req.model_dump() + self.assertEqual(payload["type"], "llm-native") + self.assertEqual(payload["input"]["uri"], "ai://store/input.jsonl") + self.assertEqual(payload["output"]["uri"], "ai://store/output/") + self.assertEqual(payload["spec"]["provider"], "azure-openai") + self.assertEqual(payload["spec"]["model"], "gpt-4.1-mini") + + def test_none_fields_excluded(self): + req = self._make_request() + payload = req.model_dump() + # No None values should be present at any level + for value in payload.values(): + self.assertIsNotNone(value) + + def test_extra_fields_forbidden(self): + with self.assertRaises(ValidationError): + BatchCreateRequest( + type="llm-native", + input=BatchInput(uri="ai://x"), + output=BatchOutput(uri="ai://y"), + spec=BatchSpec(provider="p", model="m"), + unexpected="bad", + ) + + def test_type_must_be_llm_native(self): + with self.assertRaises(ValidationError): + BatchCreateRequest( + type="not-valid", + input=BatchInput(uri="ai://x"), + output=BatchOutput(uri="ai://y"), + spec=BatchSpec(provider="p", model="m"), + ) + + def test_required_fields(self): + with self.assertRaises(ValidationError): + BatchCreateRequest(type="llm-native") + + def test_batch_create_response(self): + data = { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "created_at": "2026-04-30T10:00:00Z", + "status": "PENDING", + "message": "Batch job scheduled", + } + resp = BatchCreateResponse(**data) + self.assertEqual(resp.id, data["id"]) + self.assertEqual(resp.status, "PENDING") + self.assertEqual(resp.message, "Batch job scheduled") + + def test_batch_create_response_allows_extra_fields(self): + data = {"id": "abc", "status": "PENDING", "future_field": "value"} + resp = BatchCreateResponse(**data) + self.assertEqual(resp.id, "abc") + + def test_batch_list_response(self): + data = { + "count": 2, + "resources": [ + {"id": "id1", "type": "llm-native", "provider": "azure-openai", + "created_at": "2026-04-30T10:00:00Z", "status": "COMPLETED"}, + {"id": "id2", "type": "llm-native", "provider": "azure-openai", + "created_at": "2026-04-30T11:00:00Z", "status": "RUNNING"}, + ], + } + resp = BatchListResponse(**data) + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + self.assertIsInstance(resp.resources[0], BatchSummary) + self.assertEqual(resp.resources[0].id, "id1") + self.assertEqual(resp.resources[1].status, "RUNNING") + + def test_batch_detail_response_nested_status(self): + data = { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "llm-native", + "provider": "azure-openai", + "created_at": "2026-04-30T10:00:00Z", + "input": {"uri": "ai://store/input.jsonl"}, + "output": {"uri": "ai://store/output/"}, + "spec": {"model": "gpt-4.1-mini"}, + "status": { + "current_status": "COMPLETED", + "target_status": "COMPLETED", + "updated_at": "2026-04-30T12:00:00Z", + "message": None, + }, + } + resp = BatchDetailResponse(**data) + self.assertEqual(resp.id, data["id"]) + self.assertEqual(resp.status.current_status, "COMPLETED") + self.assertIsNone(resp.status.message) + self.assertEqual(resp.input.uri, "ai://store/input.jsonl") + self.assertEqual(resp.output.uri, "ai://store/output/") + + def test_batch_status_response(self): + data = { + "current_status": "RUNNING", + "target_status": "COMPLETED", + "updated_at": "2026-04-30T11:30:00Z", + "message": None, + } + resp = BatchStatusResponse(**data) + self.assertEqual(resp.current_status, "RUNNING") + self.assertEqual(resp.target_status, "COMPLETED") + self.assertIsNone(resp.message) + + def test_batch_cancel_response(self): + data = { + "id": "a1b2c3d4", + "created_at": "2026-04-30T10:00:00Z", + "message": "Batch job scheduled for cancellation", + } + resp = BatchCancelResponse(**data) + self.assertEqual(resp.message, "Batch job scheduled for cancellation") + + def test_batch_delete_response(self): + data = { + "id": "a1b2c3d4", + "created_at": "2026-04-30T10:00:00Z", + "message": "Batch job deleted successfully", + } + resp = BatchDeleteResponse(**data) + self.assertEqual(resp.message, "Batch job deleted successfully") + + def test_error_response(self): + data = { + "request_id": "d4a67ea1-2bf9-4df7-8105-d48203ccff76", + "message": "Batch job not found", + } + resp = ErrorResponse(**data) + self.assertEqual(resp.request_id, data["request_id"]) + self.assertEqual(resp.message, "Batch job not found") diff --git a/packages/gen/tests/document_grounding/__init__.py b/packages/gen/tests/document_grounding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/document_grounding/pipeline/__init__.py b/packages/gen/tests/document_grounding/pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/document_grounding/pipeline/mock.py b/packages/gen/tests/document_grounding/pipeline/mock.py new file mode 100644 index 0000000..054d4da --- /dev/null +++ b/packages/gen/tests/document_grounding/pipeline/mock.py @@ -0,0 +1,112 @@ +from gen_ai_hub.document_grounding.models.pipeline import ( + GetPipelinesResponse, + S3PipelineGetResponse, + CommonConfiguration, + SFTPPipelineGetResponse, + BasePipelineResponse, + GetPipelineStatusResponse, + PipelineIdResponse, + SearchPipelinesResponse, + SearchPipelineData, + SearchPipelineRequest, + DataRepositoryMetadataItem, + GetPipelineExecutionsResponse, + PipelineExecution, + DocumentsStatusResponse, + Document, + ManualPipelineTrigger, +) + +"""Document Grounding test constants""" +PATH_PIPELINES_API_ = "/lm/document-grounding/pipelines" +PIPELINE_ID = "123" + +BASE_PIPELINE_RESPONSE = BasePipelineResponse(id=PIPELINE_ID, type="S3") +GET_PIPELINES_RESPONSE = GetPipelinesResponse(count=2, resources=[ + S3PipelineGetResponse(id="testS3", configuration=CommonConfiguration(destination="s3-secret")), + SFTPPipelineGetResponse(id="testSFTP", configuration=CommonConfiguration(destination="sftp-secret")) + ]) + +GET_PIPELINE_STATUS_RESPONSE = GetPipelineStatusResponse(status="FINISHED", lastStarted="2025-10-10T00:00:00Z") +PIPELINE_ID_RESPONSE = PipelineIdResponse(pipelineId=PIPELINE_ID) + +# ---- Search pipelines ---- +SEARCH_PIPELINES_REQUEST = SearchPipelineRequest( + dataRepositoryMetadata=[ + DataRepositoryMetadataItem(key="new1", value=["details"]) + ] +) + +SEARCH_PIPELINES_RESPONSE = SearchPipelinesResponse( + count=1, + resources=[SearchPipelineData(pipelineId=PIPELINE_ID)] +) + +# ---- Executions (pipeline runs) ---- +EXECUTION_ID_1 = "exec-1" +EXECUTION_ID_2 = "exec-2" + +GET_EXECUTIONS_RESPONSE = GetPipelineExecutionsResponse( + count=2, + resources=[ + PipelineExecution(id=EXECUTION_ID_1, status="FINISHED"), + PipelineExecution(id=EXECUTION_ID_2, status="INPROGRESS"), + ], +) + +GET_EXECUTION_BY_ID_RESPONSE = PipelineExecution( + id=EXECUTION_ID_1, + status="FINISHED", +) + +# ---- Documents for a specific execution ---- +DOCUMENT_ID_1 = "doc-1" +DOCUMENT_ID_2 = "doc-2" + +DOC_EXEC_1 = Document( + id=DOCUMENT_ID_1, + status="INDEXED", + title="Execution Document 1", +) + +DOC_EXEC_2 = Document( + id=DOCUMENT_ID_2, + status="FAILED", + title="Execution Document 2", +) + +GET_EXECUTION_DOCUMENTS_RESPONSE = DocumentsStatusResponse( + count=2, + resources=[DOC_EXEC_1, DOC_EXEC_2], +) + +GET_EXECUTION_DOCUMENT_BY_ID_RESPONSE = DOC_EXEC_1 + +# ---- Documents for a pipeline (regardless of execution) ---- +DOCUMENT_ID_3 = "doc-3" +DOCUMENT_ID_4 = "doc-4" + +DOC_PIPELINE_1 = Document( + id=DOCUMENT_ID_3, + status="REINDEXED", + title="Pipeline Document A", +) + +DOC_PIPELINE_2 = Document( + id=DOCUMENT_ID_4, + status="TO_BE_PROCESSED", + title="Pipeline Document B", +) + +GET_PIPELINE_DOCUMENTS_RESPONSE = DocumentsStatusResponse( + count=2, + resources=[DOC_PIPELINE_1, DOC_PIPELINE_2], +) + +GET_PIPELINE_DOCUMENT_BY_ID_RESPONSE = DOC_PIPELINE_2 + +# ---- Manual trigger ---- +MANUAL_TRIGGER_REQUEST = ManualPipelineTrigger( + pipelineId=PIPELINE_ID, + metadataOnly=True, +) \ No newline at end of file diff --git a/packages/gen/tests/document_grounding/pipeline/test_client.py b/packages/gen/tests/document_grounding/pipeline/test_client.py new file mode 100644 index 0000000..7de86dc --- /dev/null +++ b/packages/gen/tests/document_grounding/pipeline/test_client.py @@ -0,0 +1,223 @@ +import unittest +from unittest.mock import patch + +import requests +from gen_ai_hub.document_grounding.client import PipelineAPIClient +from gen_ai_hub.document_grounding.models.pipeline import S3PipelineCreateRequest, CommonConfiguration +from tests.mock import get_mocked_ai_core_client + +from .mock import ( + GET_PIPELINE_STATUS_RESPONSE, + PIPELINE_ID_RESPONSE, + GET_PIPELINES_RESPONSE, + PATH_PIPELINES_API_, + BASE_PIPELINE_RESPONSE, + PIPELINE_ID, + SEARCH_PIPELINES_REQUEST, + SEARCH_PIPELINES_RESPONSE, + GET_EXECUTIONS_RESPONSE, + GET_EXECUTION_BY_ID_RESPONSE, + EXECUTION_ID_1, + GET_EXECUTION_DOCUMENTS_RESPONSE, + GET_EXECUTION_DOCUMENT_BY_ID_RESPONSE, + DOCUMENT_ID_1, + GET_PIPELINE_DOCUMENTS_RESPONSE, + GET_PIPELINE_DOCUMENT_BY_ID_RESPONSE, + DOCUMENT_ID_4, + MANUAL_TRIGGER_REQUEST, +) + + +class TestPipelineAPIClient(unittest.TestCase): + + def setUp(self): + proxy_client = get_mocked_ai_core_client(client_id='test') + self.test_client = PipelineAPIClient(proxy_client=proxy_client) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipelines(self, mock_get): + mock_get.return_value = GET_PIPELINES_RESPONSE.model_dump() + response = self.test_client.get_pipelines( + top=10, + skip=5, + count=True, + ) + self.assertEqual(response, GET_PIPELINES_RESPONSE) + self.assertEqual(response.count, 2) + mock_get.assert_called_once_with( + path=PATH_PIPELINES_API_, + params={"$top": 10, "$skip": 5, "$count": True} + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_by_id(self, mock_get): + mock_get.return_value = BASE_PIPELINE_RESPONSE.model_dump() + response = self.test_client.get_pipeline_by_id(PIPELINE_ID) + self.assertEqual(response, BASE_PIPELINE_RESPONSE) + self.assertEqual(response.id, BASE_PIPELINE_RESPONSE.id) + mock_get.assert_called_once_with(path=PATH_PIPELINES_API_ + f'/{PIPELINE_ID}') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_status(self, mock_get): + mock_get.return_value = GET_PIPELINE_STATUS_RESPONSE.model_dump() + response = self.test_client.get_pipeline_status(PIPELINE_ID) + self.assertEqual(response, GET_PIPELINE_STATUS_RESPONSE) + self.assertEqual(response.status, 'FINISHED') + mock_get.assert_called_once_with(path=PATH_PIPELINES_API_ + f'/{PIPELINE_ID}/status') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_pipeline(self, mock_post): + ppl_req = S3PipelineCreateRequest(configuration=CommonConfiguration(destination="s3-secret")) + mock_post.return_value = PIPELINE_ID_RESPONSE.model_dump() + response = self.test_client.create_pipeline(pipeline_request=ppl_req) + self.assertEqual(response, PIPELINE_ID_RESPONSE) + mock_post.assert_called_once_with(path=PATH_PIPELINES_API_, body=ppl_req.model_dump(exclude_none=True)) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.delete') + def test_delete_pipeline_by_id(self, mock_delete=None): + mock_delete.return_value = "" + response = self.test_client.delete_pipeline_by_id(PIPELINE_ID) + self.assertEqual(response.status_code, 204) + mock_delete.assert_called_once_with(path=PATH_PIPELINES_API_ + f'/{PIPELINE_ID}') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_search_pipelines(self, mock_post): + mock_post.return_value = SEARCH_PIPELINES_RESPONSE.model_dump() + resp = self.test_client.search_pipelines(SEARCH_PIPELINES_REQUEST) + + self.assertEqual(resp, SEARCH_PIPELINES_RESPONSE) + self.assertEqual(resp.count, 1) + self.assertEqual(resp.resources[0].pipelineId, PIPELINE_ID) + + mock_post.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/search", + body=SEARCH_PIPELINES_REQUEST.model_dump(exclude_none=True), + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_executions_no_params(self, mock_get): + mock_get.return_value = GET_EXECUTIONS_RESPONSE.model_dump() + resp = self.test_client.get_pipeline_executions(pipeline_id=PIPELINE_ID) + + self.assertEqual(resp, GET_EXECUTIONS_RESPONSE) + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/executions", + params={}, + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_executions_with_params(self, mock_get): + mock_get.return_value = GET_EXECUTIONS_RESPONSE.model_dump() + resp = self.test_client.get_pipeline_executions( + pipeline_id=PIPELINE_ID, + last_execution=True, + top=10, + skip=5, + count=True, + ) + + self.assertEqual(resp.count, 2) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/executions", + params={"lastExecution": True, "$top": 10, "$skip": 5, "$count": True}, + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_execution_by_id(self, mock_get): + mock_get.return_value = GET_EXECUTION_BY_ID_RESPONSE.model_dump() + resp = self.test_client.get_pipeline_execution_by_id( + pipeline_id=PIPELINE_ID, + execution_id=EXECUTION_ID_1, + ) + + self.assertEqual(resp, GET_EXECUTION_BY_ID_RESPONSE) + self.assertEqual(resp.id, EXECUTION_ID_1) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/executions/{EXECUTION_ID_1}" + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_execution_documents(self, mock_get): + mock_get.return_value = GET_EXECUTION_DOCUMENTS_RESPONSE.model_dump() + resp = self.test_client.get_execution_documents( + pipeline_id=PIPELINE_ID, + execution_id=EXECUTION_ID_1, + top=50, + skip=0, + count=True, + ) + + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/executions/{EXECUTION_ID_1}/documents", + params={"$top": 50, "$skip": 0, "$count": True}, + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_execution_document_by_id(self, mock_get): + mock_get.return_value = GET_EXECUTION_DOCUMENT_BY_ID_RESPONSE.model_dump() + resp = self.test_client.get_execution_document_by_id( + pipeline_id=PIPELINE_ID, + execution_id=EXECUTION_ID_1, + document_id=DOCUMENT_ID_1, + ) + + self.assertEqual(resp, GET_EXECUTION_DOCUMENT_BY_ID_RESPONSE) + self.assertEqual(resp.id, DOCUMENT_ID_1) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/executions/{EXECUTION_ID_1}/documents/{DOCUMENT_ID_1}" + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_documents(self, mock_get): + mock_get.return_value = GET_PIPELINE_DOCUMENTS_RESPONSE.model_dump() + resp = self.test_client.get_pipeline_documents( + pipeline_id=PIPELINE_ID, + top=25, + skip=5, + count=False, + ) + + self.assertEqual(resp.count, 2) + self.assertEqual(len(resp.resources), 2) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/documents", + params={"$top": 25, "$skip": 5, "$count": False}, + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_pipeline_document_by_id(self, mock_get): + mock_get.return_value = GET_PIPELINE_DOCUMENT_BY_ID_RESPONSE.model_dump() + resp = self.test_client.get_pipeline_document_by_id( + pipeline_id=PIPELINE_ID, + document_id=DOCUMENT_ID_4, + ) + + self.assertEqual(resp, GET_PIPELINE_DOCUMENT_BY_ID_RESPONSE) + self.assertEqual(resp.id, DOCUMENT_ID_4) + + mock_get.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/{PIPELINE_ID}/documents/{DOCUMENT_ID_4}" + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_trigger_pipeline_empty_string_returns_202(self, mock_post): + mock_post.return_value = "" + resp = self.test_client.trigger_pipeline(MANUAL_TRIGGER_REQUEST) + + self.assertIsInstance(resp, requests.Response) + self.assertEqual(resp.status_code, 202) + + mock_post.assert_called_once_with( + path=f"{PATH_PIPELINES_API_}/trigger", + body=MANUAL_TRIGGER_REQUEST.model_dump(exclude_none=True), + ) diff --git a/packages/gen/tests/document_grounding/retrieval/__init__.py b/packages/gen/tests/document_grounding/retrieval/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/document_grounding/retrieval/mock.py b/packages/gen/tests/document_grounding/retrieval/mock.py new file mode 100644 index 0000000..028b2b8 --- /dev/null +++ b/packages/gen/tests/document_grounding/retrieval/mock.py @@ -0,0 +1,133 @@ +from gen_ai_hub.document_grounding.models.retrieval import ( + RetrievalKeyValueListPair, + RetrievalDocumentKeyValueListPair, + RetrievalSearchDocumentKeyValueListPair, + RetrievalChunk, + RetrievalDocument, + RetrievalSearchInput, + RetrievalSearchFilter, + RetrievalSearchConfiguration, + RetrievalDataRepositorySearchResult, + RetrievalPerFilterSearchResult, + RetrievalSearchResults, + DataRepository, + DataRepositoryWithDocuments, + DataRepositories, + DataRepository, +) + +"""Document Grounding Retrieval API test constants""" + +PATH_RETRIEVAL_API_ = "/lm/document-grounding/retrieval" + +# --- Data Repositories --- + +DATA_REPOSITORY_1 = DataRepository( + id="4be1c754-ad62-4030-ac1c-498312327c23", + title="test-canary-collection", + type="vector", + metadata=[ + RetrievalKeyValueListPair(key="purpose", value=["demonstration"]), + RetrievalKeyValueListPair(key="a-random-key", value=["hello world!"]) + ] +) + +DATA_REPOSITORY_2 = DataRepository( + id="101fd17c-10f5-4f5d-add9-a07e88a4d75d", + title="SAP Help Portal - help.sap.com", + type="help.sap.com", + metadata=[] +) + +DATA_REPOSITORIES_RESPONSE = DataRepositories( + count=2, + resources=[DATA_REPOSITORY_1, DATA_REPOSITORY_2] +) + +DATA_REPOSITORY_RESPONSE = DataRepository( + id="6abcf8a7-98d0-44e2-9735-f6c1ad056591", + title="test-canary-collection", + type="vector", + metadata=[ + RetrievalKeyValueListPair(key="purpose", value=["demonstration"]), + RetrievalKeyValueListPair(key="a-random-key", value=["hello world!"]) + ] +) + +# --- Retrieval Search Input --- + +RETRIEVAL_SEARCH_INPUT = RetrievalSearchInput( + query="is Joule an AI Copilot?", + filters=[ + RetrievalSearchFilter( + id="string", + dataRepositoryType="vector", + searchConfiguration=RetrievalSearchConfiguration( + maxChunkCount=None, + maxDocumentCount=None + ), + dataRepositories=["4be1c754-ad62-4030-ac1c-498312327c23"], + dataRepositoryMetadata=[], + documentMetadata=[ + RetrievalSearchDocumentKeyValueListPair( + key="url", + value=["http://hello1.com"], + selectMode=["ignoreIfKeyAbsent"] + ) + ], + chunkMetadata=[] + ) + ] +) + +# --- Retrieval Search Results --- + +RETRIEVAL_SEARCH_RESULT = RetrievalSearchResults( + results=[ + RetrievalPerFilterSearchResult( + filterId="string", + results=[ + RetrievalDataRepositorySearchResult( + dataRepository=DataRepositoryWithDocuments( + id="4be1c754-ad62-4030-ac1c-498312327c23", + title="test-canary-collection", + metadata=[ + RetrievalKeyValueListPair(key="purpose", value=["demonstration"]), + RetrievalKeyValueListPair(key="a-random-key", value=["hello world!"]) + ], + documents=[ + RetrievalDocument( + id="3e598574-e6b1-4a1c-a601-d9c54a9a1e47", + metadata=[ + RetrievalDocumentKeyValueListPair( + key="url", + value=["http://hello1.com"], + matchMode="ANY" + ) + ], + chunks=[ + RetrievalChunk( + id="dd5c7ffc-7fae-4d0f-b5f0-527fefe5e416", + content="Joule is not the AI copilot that truly understands your business. Joule revolutionizes how you interact with your SAP business systems.", + metadata=[ + RetrievalKeyValueListPair(key="index", value=["1"]), + RetrievalKeyValueListPair(key="sap.document-grounding/language", value=["en"]) + ] + ), + RetrievalChunk( + id="829508cd-68e3-43bf-9faa-84c0bbc616b5", + content="It enables the companion of the Intelligent Enterprise, guiding you through content discovery within SAP Ecosystem.", + metadata=[ + RetrievalKeyValueListPair(key="index", value=["2"]), + RetrievalKeyValueListPair(key="sap.document-grounding/language", value=["en"]) + ] + ) + ] + ) + ] + ) + ) + ] + ) + ] +) diff --git a/packages/gen/tests/document_grounding/retrieval/test_client.py b/packages/gen/tests/document_grounding/retrieval/test_client.py new file mode 100644 index 0000000..57a28ce --- /dev/null +++ b/packages/gen/tests/document_grounding/retrieval/test_client.py @@ -0,0 +1,52 @@ +import unittest +from unittest.mock import patch + +from gen_ai_hub.document_grounding.clients.retrieval_api_client import RetrievalAPIClient +from tests.mock import get_mocked_ai_core_client + +from .mock import ( + PATH_RETRIEVAL_API_, + DATA_REPOSITORIES_RESPONSE, + DATA_REPOSITORY_RESPONSE, + RETRIEVAL_SEARCH_INPUT, + RETRIEVAL_SEARCH_RESULT, +) + + +class TestRetrievalAPIClient(unittest.TestCase): + + def setUp(self): + proxy_client = get_mocked_ai_core_client(client_id='test') + self.test_client = RetrievalAPIClient(proxy_client=proxy_client) + + # --- Data Repositories --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_data_repositories(self, mock_get): + """List all data repositories should return structured DataRepositoriesResponse""" + mock_get.return_value = DATA_REPOSITORIES_RESPONSE.model_dump() + response = self.test_client.get_data_repositories() + self.assertEqual(response, DATA_REPOSITORIES_RESPONSE) + mock_get.assert_called_once_with(path=f"{PATH_RETRIEVAL_API_}/dataRepositories", params={}) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_data_repository_by_id(self, mock_get): + """Get single data repository by ID should return DataRepository""" + repository_id = DATA_REPOSITORY_RESPONSE.id + mock_get.return_value = DATA_REPOSITORY_RESPONSE.model_dump() + response = self.test_client.get_data_repository_by_id(repository_id) + self.assertEqual(response, DATA_REPOSITORY_RESPONSE) + mock_get.assert_called_once_with(path=f"{PATH_RETRIEVAL_API_}/dataRepositories/{repository_id}") + + # --- Search --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_search(self, mock_post): + """Retrieval search should return structured RetrievalSearchResults""" + mock_post.return_value = RETRIEVAL_SEARCH_RESULT.model_dump() + response = self.test_client.search(RETRIEVAL_SEARCH_INPUT) + self.assertEqual(response, RETRIEVAL_SEARCH_RESULT) + mock_post.assert_called_once_with( + path=f"{PATH_RETRIEVAL_API_}/search", + body=RETRIEVAL_SEARCH_INPUT.model_dump(exclude_none=True) + ) \ No newline at end of file diff --git a/packages/gen/tests/document_grounding/test_flat_import_document_grounding.py b/packages/gen/tests/document_grounding/test_flat_import_document_grounding.py new file mode 100644 index 0000000..08e002a --- /dev/null +++ b/packages/gen/tests/document_grounding/test_flat_import_document_grounding.py @@ -0,0 +1,110 @@ +expected = { # Pipeline models + "CreatePipelineRequest", + "MSSharePointPipelineCreateRequest", + "S3PipelineCreateRequest", + "SFTPPipelineCreateRequest", + "SearchPipelineRequest", + "DataRepositoryMetadataItem", + "CommonConfiguration", + "MetaData", + "MSSharePointConfiguration", + "SharePointConfig", + "SharePointSite", + "ManualPipelineTrigger", + "PipelineIdResponse", + "GetPipelineResponse", + "GetPipelinesResponse", + "GetPipelineStatusResponse", + "PipelineExecution", + "GetPipelineExecutionsResponse", + "Document", + "DocumentsStatusResponse", + "MSSharePointPipelineGetResponse", + "S3PipelineGetResponse", + "SFTPPipelineGetResponse", + "SearchPipelineData", + "SearchPipelinesResponse", + "PipelineExecutionStatus", + "DocumentStatus", + "BasePipelineResponse", + "MSSharePointConfigurationGetResponse", + # Retrieval models + "RetrievalKeyValueListPair", + "RetrievalDocumentKeyValueListPair", + "RetrievalSearchDocumentKeyValueListPair", + "RetrievalChunk", + "RetrievalDocument", + "DataRepositoryType", + "DataRepository", + "DataRepositoryWithDocuments", + "RetrievalSearchConfiguration", + "RetrievalSearchFilter", + "RetrievalSearchInput", + "RetrievalDataRepositorySearchResult", + "RetrievalPerFilterSearchResult", + "RetrievalPerFilterSearchResultError", + "RetrievalPerFilterSearchResultWithError", + "RetrievalSearchResults", + "DataRepositories", + # Vector models + "VectorKeyValueListPair", + "EmbeddingConfig", + "CollectionCreateRequest", + "Collection", + "CollectionsListResponse", + "TextOnlyBaseChunk", + "BaseDocument", + "DocumentWithoutChunks", + "VectorDocument", + "DocumentsCreateRequest", + "DocumentsUpdateRequest", + "DocumentsListResponse", + "DocumentsResponse", + "CollectionCreatedResponse", + "CollectionDeletedResponse", + "CollectionPendingResponse", + "CollectionCreationStatusResponse", + "CollectionDeletionStatusResponse", + "VectorSearchConfiguration", + "VectorSearchDocumentKeyValueListPair", + "VectorSearchFilter", + "TextSearchRequest", + "VectorChunk", + "DocumentOutput", + "DocumentsChunk", + "VectorPerFilterSearchResult", + "VectorSearchResults", + # Clients + "PipelineAPIClient", + "RetrievalAPIClient", + "VectorAPIClient" +} + +def test_flat_import_document_grounding_all(): + import gen_ai_hub.document_grounding as module + assert set(module.__all__) == expected + +def test_flat_import_document_grounding_by_name(): + from gen_ai_hub.document_grounding import PipelineAPIClient as client_flat + from gen_ai_hub.document_grounding.client import PipelineAPIClient as client + assert client_flat == client + + from gen_ai_hub.document_grounding import VectorAPIClient as vector_client_flat + from gen_ai_hub.document_grounding.client import VectorAPIClient as vector_client + assert vector_client_flat == vector_client + + from gen_ai_hub.document_grounding import RetrievalAPIClient as retrieval_client_flat + from gen_ai_hub.document_grounding.client import RetrievalAPIClient as retrieval_client + assert retrieval_client_flat == retrieval_client + + from gen_ai_hub.document_grounding import CreatePipelineRequest as create_pipeline_request_flat + from gen_ai_hub.document_grounding.models.pipeline import CreatePipelineRequest as create_pipeline_request + assert create_pipeline_request_flat == create_pipeline_request + + from gen_ai_hub.document_grounding import RetrievalSearchConfiguration as retrieval_search_configuration_flat + from gen_ai_hub.document_grounding.models.retrieval import RetrievalSearchConfiguration as retrieval_search_configuration + assert retrieval_search_configuration_flat == retrieval_search_configuration + + from gen_ai_hub.document_grounding import DocumentsCreateRequest as documents_create_request_flat + from gen_ai_hub.document_grounding.models.vector import DocumentsCreateRequest as documents_create_request + assert documents_create_request_flat == documents_create_request diff --git a/packages/gen/tests/document_grounding/vector/__init__.py b/packages/gen/tests/document_grounding/vector/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/document_grounding/vector/mock.py b/packages/gen/tests/document_grounding/vector/mock.py new file mode 100644 index 0000000..4b7542e --- /dev/null +++ b/packages/gen/tests/document_grounding/vector/mock.py @@ -0,0 +1,179 @@ +import requests +from gen_ai_hub.document_grounding.models.vector import ( + Collection, + CollectionsListResponse, + CollectionCreateRequest, + CollectionCreatedResponse, + CollectionDeletedResponse, + CollectionPendingResponse, + DocumentsCreateRequest, + DocumentsUpdateRequest, + DocumentsListResponse, + Document, + DocumentsResponse, + VectorKeyValueListPair, + TextOnlyBaseChunk, + BaseDocument, + VectorSearchResults, + VectorPerFilterSearchResult, + DocumentsChunk, + DocumentOutput, + DocumentWithoutChunks, + VectorChunk, + TextSearchRequest, + VectorSearchFilter, + VectorSearchConfiguration, + VectorSearchDocumentKeyValueListPair, + +) + +"""Document Grounding Vector API test constants""" + +PATH_VECTOR_API_ = "/lm/document-grounding/vector" +COLLECTION_ID = "84f4f74b-8df9-4c73-8f2d-5729b24dd6eb" +DOCUMENT_ID = "0fb9878d-72fe-4267-8f4b-f68947729aab" + +# --- Collections --- +COLLECTION = Collection( + id=COLLECTION_ID, + title="test-collection", + embeddingConfig={"modelName": "text-embedding-3-large"}, + metadata=[VectorKeyValueListPair(key="purpose", value=["testing"])] +) + +COLLECTIONS_LIST_RESPONSE = CollectionsListResponse(count=1, resources=[COLLECTION]) + +COLLECTION_CREATE_REQUEST = CollectionCreateRequest( + title="test-collection", + embeddingConfig={"modelName": "text-embedding-3-large"}, + metadata=[VectorKeyValueListPair(key="purpose", value=["testing"])] +) + +COLLECTION_CREATED_RESPONSE = CollectionCreatedResponse(collectionUrl=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}") +COLLECTION_PENDING_RESPONSE = CollectionPendingResponse(location=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/status") +COLLECTION_DELETED_RESPONSE = CollectionDeletedResponse(collectionUrl=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}") + +# --- Documents --- +CHUNK_1 = TextOnlyBaseChunk( + content="This is a test chunk", + metadata=[VectorKeyValueListPair(key="index", value=["1"])] +) +CHUNK_2 = TextOnlyBaseChunk( + content="Another test chunk", + metadata=[VectorKeyValueListPair(key="index", value=["2"])] +) + +VECTOR_CHUNK_1 = VectorChunk( + id="chunk-1", + content="This is a test chunk", + metadata=[VectorKeyValueListPair(key="index", value=["1"])] +) + +VECTOR_CHUNK_2 = VectorChunk( + id="chunk-2", + content="Another test chunk", + metadata=[VectorKeyValueListPair(key="index", value=["2"])] +) + +BASE_DOCUMENT = BaseDocument( + chunks=[CHUNK_1, CHUNK_2], + metadata=[VectorKeyValueListPair(key="url", value=["http://example.com"])] +) + +DOCUMENT = Document( + id=DOCUMENT_ID, + chunks=[CHUNK_1, CHUNK_2], + metadata=[VectorKeyValueListPair(key="url", value=["http://example.com"])] +) + +DOCUMENTS_CREATE_REQUEST = DocumentsCreateRequest(documents=[BASE_DOCUMENT]) +DOCUMENTS_UPDATE_REQUEST = DocumentsUpdateRequest(documents=[DOCUMENT]) + +DOCUMENT_WITHOUT_CHUNKS = DocumentWithoutChunks( + id="0fb9878d-72fe-4267-8f4b-f68947729aab", + metadata=[VectorKeyValueListPair(key="url", value=["http://example.com"])] +) + +DOCUMENTS_LIST_RESPONSE = DocumentsListResponse(documents=[DOCUMENT_WITHOUT_CHUNKS]) +DOCUMENTS_RESPONSE = DocumentsResponse(count=1, resources=[DOCUMENT_WITHOUT_CHUNKS]) + +# --- Vector Search --- +VECTOR_SEARCH_REQUEST = TextSearchRequest( + query="is Joule an AI Copilot?", + filters=[ + VectorSearchFilter( + id="filter-001", + collectionIds=["84f4f74b-8df9-4c73-8f2d-5729b24dd6eb"], + configuration=VectorSearchConfiguration( + maxChunkCount=5, + maxDocumentCount=2 + ), + collectionMetadata=[ + VectorKeyValueListPair(key="domain", value=["sap", "ai"]), + VectorKeyValueListPair(key="environment", value=["production"]) + ], + documentMetadata=[ + VectorSearchDocumentKeyValueListPair( + key="author", + value=["SAP Labs"], + selectMode=["INCLUDE"] + ) + ], + chunkMetadata=[ + VectorKeyValueListPair(key="language", value=["en"]), + VectorKeyValueListPair(key="topic", value=["copilot"]) + ] + ) + ] +) +VECTOR_SEARCH_RESPONSE = VectorSearchResults( + results=[ + VectorPerFilterSearchResult( + filterId="filter-001", + results=[ + DocumentsChunk( + id="84f4f74b-8df9-4c73-8f2d-5729b24dd6eb", + title="SAP Joule Overview", + metadata=[ + VectorKeyValueListPair(key="domain", value=["sap", "ai"]), + VectorKeyValueListPair(key="environment", value=["production"]) + ], + documents=[ + DocumentOutput( + id="0fb9878d-72fe-4267-8f4b-f68947729aab", + metadata=[ + VectorKeyValueListPair(key="author", value=["SAP Labs"]), + VectorKeyValueListPair(key="language", value=["en"]) + ], + chunks=[ + VectorChunk( + id="chunk-001", + content="Joule is the AI copilot that understands your business context deeply.", + metadata=[ + VectorKeyValueListPair(key="topic", value=["copilot"]), + VectorKeyValueListPair(key="index", value=["1"]) + ] + ), + VectorChunk( + id="chunk-002", + content="It integrates across SAP systems to streamline workflows and enhance productivity.", + metadata=[ + VectorKeyValueListPair(key="topic", value=["copilot"]), + VectorKeyValueListPair(key="index", value=["2"]) + ] + ) + ] + ) + ] + ) + ] + ) + ] +) + +# --- Common Responses --- +RESPONSE_202 = requests.Response() +RESPONSE_202.status_code = 202 + +RESPONSE_204 = requests.Response() +RESPONSE_204.status_code = 204 diff --git a/packages/gen/tests/document_grounding/vector/test_client.py b/packages/gen/tests/document_grounding/vector/test_client.py new file mode 100644 index 0000000..c9fd93a --- /dev/null +++ b/packages/gen/tests/document_grounding/vector/test_client.py @@ -0,0 +1,146 @@ +import unittest +from unittest.mock import patch + +from gen_ai_hub.document_grounding.clients.vector_api_client import VectorAPIClient +from tests.mock import get_mocked_ai_core_client + +from .mock import ( + PATH_VECTOR_API_, + COLLECTION_ID, + DOCUMENT_ID, + COLLECTION, + COLLECTIONS_LIST_RESPONSE, + COLLECTION_CREATE_REQUEST, + COLLECTION_CREATED_RESPONSE, + COLLECTION_DELETED_RESPONSE, + DOCUMENTS_CREATE_REQUEST, + DOCUMENTS_UPDATE_REQUEST, + DOCUMENTS_LIST_RESPONSE, + DOCUMENTS_RESPONSE, + DOCUMENT, + VECTOR_SEARCH_REQUEST, + VECTOR_SEARCH_RESPONSE, + RESPONSE_202, + RESPONSE_204, +) + + +class TestVectorAPIClient(unittest.TestCase): + + def setUp(self): + proxy_client = get_mocked_ai_core_client(client_id='test') + self.test_client = VectorAPIClient(proxy_client=proxy_client) + + # --- Collections --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_collections(self, mock_get): + mock_get.return_value = COLLECTIONS_LIST_RESPONSE.model_dump() + response = self.test_client.get_collections() + self.assertEqual(response, COLLECTIONS_LIST_RESPONSE) + mock_get.assert_called_once_with(path=f"{PATH_VECTOR_API_}/collections", params={}) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_collection_by_id(self, mock_get): + mock_get.return_value = COLLECTION.model_dump() + response = self.test_client.get_collection_by_id(COLLECTION_ID) + self.assertEqual(response, COLLECTION) + mock_get.assert_called_once_with(path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}") + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_collection_returns_202(self, mock_post): + """Create collection should return Response 202""" + mock_post.return_value = "" + response = self.test_client.create_collection(COLLECTION_CREATE_REQUEST) + self.assertEqual(response.status_code, RESPONSE_202.status_code) + mock_post.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections", + body=COLLECTION_CREATE_REQUEST.model_dump(exclude_none=True) + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.delete') + def test_delete_collection_returns_204(self, mock_delete): + """Delete collection should return Response 204""" + mock_delete.return_value = "" + response = self.test_client.delete_collection(COLLECTION_ID) + self.assertEqual(response.status_code, RESPONSE_204.status_code) + mock_delete.assert_called_once_with(path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}") + + # --- Documents --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_documents(self, mock_get): + mock_get.return_value = DOCUMENTS_RESPONSE.model_dump() + response = self.test_client.get_documents(COLLECTION_ID) + self.assertEqual(response, DOCUMENTS_RESPONSE) + mock_get.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/documents", + params={} + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_document_by_id(self, mock_get): + mock_get.return_value = DOCUMENT.model_dump() + response = self.test_client.get_document_by_id(COLLECTION_ID, DOCUMENT_ID) + self.assertEqual(response, DOCUMENT) + mock_get.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/documents/{DOCUMENT_ID}" + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_documents(self, mock_post): + mock_post.return_value = DOCUMENTS_LIST_RESPONSE.model_dump() + response = self.test_client.create_documents(COLLECTION_ID, DOCUMENTS_CREATE_REQUEST) + self.assertEqual(response, DOCUMENTS_LIST_RESPONSE) + mock_post.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/documents", + body=DOCUMENTS_CREATE_REQUEST.model_dump(exclude_none=True) + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.patch') + def test_update_documents(self, mock_patch): + mock_patch.return_value = DOCUMENTS_LIST_RESPONSE.model_dump() + response = self.test_client.update_documents(COLLECTION_ID, DOCUMENTS_UPDATE_REQUEST) + self.assertEqual(response, DOCUMENTS_LIST_RESPONSE) + mock_patch.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/documents", + body=DOCUMENTS_UPDATE_REQUEST.model_dump(exclude_none=True) + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.delete') + def test_delete_document_returns_204(self, mock_delete): + mock_delete.return_value = "" + response = self.test_client.delete_document(COLLECTION_ID, DOCUMENT_ID) + self.assertEqual(response.status_code, RESPONSE_204.status_code) + mock_delete.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/documents/{DOCUMENT_ID}" + ) + + # --- Collection statuses --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_collection_creation_status(self, mock_get): + mock_get.return_value = COLLECTION_CREATED_RESPONSE.model_dump(by_alias=True) + response = self.test_client.get_collection_creation_status(COLLECTION_ID) + self.assertEqual(response.status, "CREATED") + mock_get.assert_called_once_with(path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/creationStatus") + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_collection_deletion_status(self, mock_get): + mock_get.return_value = COLLECTION_DELETED_RESPONSE.model_dump(by_alias=True) + response = self.test_client.get_collection_deletion_status(COLLECTION_ID) + self.assertEqual(response.status, "DELETED") + mock_get.assert_called_once_with(path=f"{PATH_VECTOR_API_}/collections/{COLLECTION_ID}/deletionStatus") + + # --- Search --- + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_search(self, mock_post): + """Semantic search with filters should return structured VectorSearchResults""" + mock_post.return_value = VECTOR_SEARCH_RESPONSE.model_dump() + response = self.test_client.search(request=VECTOR_SEARCH_REQUEST) + self.assertEqual(response, VECTOR_SEARCH_RESPONSE) + mock_post.assert_called_once_with( + path=f"{PATH_VECTOR_API_}/search", + body=VECTOR_SEARCH_REQUEST.model_dump(exclude_none=True) + ) \ No newline at end of file diff --git a/packages/gen/tests/evaluations/__init__.py b/packages/gen/tests/evaluations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/evaluations/test_aicore_utils.py b/packages/gen/tests/evaluations/test_aicore_utils.py new file mode 100755 index 0000000..430e8ff --- /dev/null +++ b/packages/gen/tests/evaluations/test_aicore_utils.py @@ -0,0 +1,751 @@ +import unittest +from unittest.mock import MagicMock, patch + +from ai_api_client_sdk.models.status import Status +from gen_ai_hub.evaluations.constants import ( + AWS_OSS_BUCKET_URL_KEY, + AWS_OSS_REGION_URL_KEY, + AWS_OSS_PATH_PREFIX_URL_KEY as AWS_PATH_KEY, + CSV_FILE_TYPE as CSV, + AI_PROTOCOL_PREFIX as AI_PREFIX, + DATASET_FOLDER_KEY, + SYSTEM_DEFINED_METRIC_MAPPING, +) +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.evaluations._internal._models import ( + _AWSObjectStoreData, + _EvaluationConfigData, +) +from gen_ai_hub.evaluations.utils.aicore_utils import ( + generate_random_id, + find_configuration_id_by_name, + get_all_configurations, + get_running_deployments_by_configuration_id, + create_deployment_by_configuration_id, + create_llm_orchestration_deployment_url, + wait_for_target_status, + read_data_from_artifact, + build_s3_file_key, + resolve_artifact_path, + fetch_deployment_config, + fetch_configuration_by_id, + call_orchestration_service_with_v2_config, + upload_file_to_aws_s3, + upload_evaluation_dataset_data, + register_aicore_artifact, + register_aicore_configuration, + register_aicore_execution, + list_available_llm_models, + fetch_orchestration_config_from_registry, + resolve_metric_identifiers, + resolve_metric_names, +) + +MODULE_PATH = "gen_ai_hub.evaluations.utils.aicore_utils" + +DUMMY_ORCH_CONFIG = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"content": "Hello {{?name}}", "role": "user"}], + "defaults": {"name": ""} + }, + "model": { + "name": "gpt-4o", + "version": "latest", + "params": {}, + "timeout": 100, + "max_retries": 1 + } + } + } +} + +DUMMY_METRIC_TEMPLATES = [ + { + "id": "metric-1", + "name": "test_metric", + "version": "1.0.0", + "spec": {"outputType": "numerical"} + } +] + + +class TestGenerateRandomId(unittest.TestCase): + def test_generate_random_id_format(self): + rid = generate_random_id() + self.assertIsInstance(rid, str) + self.assertEqual(len(rid), 32) # hex of uuid4 + + +class TestFindConfigurationIdByName(unittest.TestCase): + def test_find_configuration_id_by_name_found_and_not_found(self): + a = MagicMock() + a.id = "id-a" + a.name = "one" + + b = MagicMock() + b.id = "id-b" + b.name = "two" + self.assertEqual(find_configuration_id_by_name([a, b], "two"), "id-b") + self.assertIsNone(find_configuration_id_by_name([a, b], "missing")) + + +class TestGetAllConfigurations(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.configuration = MagicMock() + self.mock_ai_core_client.deployment = MagicMock() + self.mock_ai_core_client.artifact = MagicMock() + self.mock_ai_core_client.execution = MagicMock() + self.mock_ai_core_client.model = MagicMock() + self.mock_ai_core_client.rest_client = MagicMock() + self.mock_ai_core_client.artifact.get = MagicMock() + self.mock_ai_core_client.artifact.create = MagicMock() + self.mock_ai_core_client.configuration.create = MagicMock() + self.mock_ai_core_client.execution.create = MagicMock() + self.mock_ai_core_client.deployment.get = MagicMock() + + def test_get_all_configurations_success(self): + self.mock_ai_core_client.configuration.query.return_value.resources = ["cfg1", "cfg2"] + out = get_all_configurations(self.mock_ai_core_client, resource_group="rg", scenario_id="s") + self.assertEqual(out, ["cfg1", "cfg2"]) + + def test_get_all_configurations_raises_value_error(self): + self.mock_ai_core_client.configuration.query.side_effect = Exception("boom") + with self.assertRaises(ValueError): + get_all_configurations(self.mock_ai_core_client, "rg", "s") + + +class TestGetRunningDeploymentsByConfigurationId(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.deployment = MagicMock() + + def test_get_running_deployments_by_configuration_id_success(self): + self.mock_ai_core_client.deployment.query.return_value.resources = ["d1"] + out = get_running_deployments_by_configuration_id(self.mock_ai_core_client, "cfg", "rg") + self.assertEqual(out, ["d1"]) + + def test_get_running_deployments_by_configuration_id_raises(self): + self.mock_ai_core_client.deployment.query.side_effect = Exception("err") + with self.assertRaises(ValueError): + get_running_deployments_by_configuration_id(self.mock_ai_core_client, "cfg", "rg") + + +class TestWaitForTargetStatus(unittest.TestCase): + def test_wait_for_target_status_success_and_extract_url(self): + step1 = MagicMock(status=Status.PENDING) + step2 = MagicMock(status=Status.RUNNING, deployment_url="http://ok") + fetcher = MagicMock(side_effect=[step1, step2]) + + url = wait_for_target_status( + status_fetcher=fetcher, + target_status=Status.RUNNING, + extract_url=lambda r: r.deployment_url, + timeout=5, + initial_interval=0, + pending_interval=0, + ) + self.assertEqual(url, "http://ok") + + def test_wait_for_target_status_timeout_and_dead(self): + dead = MagicMock(status=Status.DEAD) + fetcher = MagicMock(return_value=dead) + result = wait_for_target_status( + status_fetcher=fetcher, + target_status=Status.RUNNING, + extract_url=None, + timeout=1, + initial_interval=0, + ) + self.assertIsNone(result) + + +class TestCreateDeploymentByConfigurationId(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.deployment = MagicMock() + self.mock_ai_core_client.configuration = MagicMock() + + @patch(f"{MODULE_PATH}.wait_for_target_status") + def test_create_deployment_by_configuration_id_happy_path(self, mock_wait): + deployment_response = MagicMock() + deployment_response.id = "dep-1" + self.mock_ai_core_client.deployment.create.return_value = deployment_response + + running_resp = MagicMock(status=getattr(Status, "RUNNING", "RUNNING"), deployment_url="http://dep") + self.mock_ai_core_client.deployment.get.return_value = running_resp + + mock_wait.return_value = "http://dep" + + url = create_deployment_by_configuration_id(self.mock_ai_core_client, "cfg", "rg") + self.assertEqual(url, "http://dep") + self.mock_ai_core_client.deployment.create.assert_called_once() + + def test_create_deployment_by_configuration_id_raises_on_failure(self): + self.mock_ai_core_client.deployment.create.side_effect = Exception("nope") + with self.assertRaises(RuntimeError): + create_deployment_by_configuration_id(self.mock_ai_core_client, "cfg", "rg") + + +class TestCreateLlmOrchestrationDeploymentUrl(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.configuration = MagicMock() + + @patch(f"{MODULE_PATH}.create_deployment_by_configuration_id") + def test_create_llm_orchestration_deployment_url_happy(self, mock_create_deployment): + self.mock_ai_core_client.configuration.create.return_value.id = "cfg-1" + mock_create_deployment.return_value = "http://deployed" + + url = create_llm_orchestration_deployment_url(self.mock_ai_core_client, resource_group="rg") + self.assertEqual(url, "http://deployed") + + def test_create_llm_orchestration_deployment_url_raises(self): + self.mock_ai_core_client.configuration.create.side_effect = Exception("fail") + with self.assertRaises(RuntimeError): + create_llm_orchestration_deployment_url(self.mock_ai_core_client, resource_group="rg") + + +class TestReadDataFromArtifact(unittest.TestCase): + def setUp(self): + self.collector = ValidationCollector() + + @patch(f"{MODULE_PATH}.S3FileClient") + def test_read_data_from_artifact_csv(self, mock_s3_client): + mock_boto = MagicMock() + mock_boto.read_csv.return_value = [["a", "b"]] + mock_s3_client.return_value = mock_boto + + aws_creds = _AWSObjectStoreData(aws_access_key_id="k", aws_secret_access_key="s") + meta = {AWS_OSS_BUCKET_URL_KEY: "bucket", AWS_OSS_REGION_URL_KEY: "r"} + out = read_data_from_artifact(aws_creds, meta, "key", CSV, self.collector) + self.assertEqual(out, [["a", "b"]]) + mock_boto.read_csv.assert_called_once_with("key") + + @patch(f"{MODULE_PATH}.S3FileClient") + def test_read_data_from_artifact_jsonl(self, mock_s3_client): + mock_boto = MagicMock() + mock_boto.read_jsonl.return_value = [{"x": 1}] + mock_s3_client.return_value = mock_boto + + aws_creds = _AWSObjectStoreData(aws_access_key_id="k", aws_secret_access_key="s") + meta = {AWS_OSS_BUCKET_URL_KEY: "bucket", AWS_OSS_REGION_URL_KEY: "r"} + out = read_data_from_artifact(aws_creds, meta, "key", "jsonl", self.collector) + self.assertEqual(out, [{"x": 1}]) + mock_boto.read_jsonl.assert_called_once_with("key") + + +class TestBuildS3FileKey(unittest.TestCase): + def test_build_s3_file_key_all_parts(self): + meta = {AWS_PATH_KEY: "prefix"} + rel = "artifact/path" + src = MagicMock(path="inner/file.csv") + self.assertEqual(build_s3_file_key(meta, rel, src), "prefix/artifact/path/inner/file.csv") + + def test_build_s3_file_key_minimal(self): + meta = {} + rel = "only/path" + src = MagicMock(path=None) + self.assertEqual(build_s3_file_key(meta, rel, src), "only/path") + + +class TestResolveArtifactPath(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.artifact = MagicMock() + self.collector = ValidationCollector() + + @patch(f"{MODULE_PATH}.read_data_from_artifact") + @patch(f"{MODULE_PATH}.fetch_object_store_secret_by_name") + def test_resolve_artifact_path_success(self, mock_fetch_secret, mock_read_data): + artifact = MagicMock() + artifact.url = AI_PREFIX + "secretName/folder/file.csv" + self.mock_ai_core_client.artifact.get.return_value = artifact + + mock_secret = MagicMock() + mock_secret.metadata = {AWS_PATH_KEY: "pref"} + mock_fetch_secret.return_value = mock_secret + mock_read_data.return_value = [{"ok": 1}] + + aws_creds = _AWSObjectStoreData(aws_access_key_id="a", aws_secret_access_key="b") + src = ArtifactSource(artifact="artifact-id", path="inner/file.csv", file_type="csv") + result = resolve_artifact_path(src, self.mock_ai_core_client, aws_creds, "rg", self.collector) + self.assertEqual(result, [{"ok": 1}]) + + @patch(f"{MODULE_PATH}.fetch_object_store_secret_by_name") + def test_resolve_artifact_path_bad_url(self, mock_fetch_secret): + artifact = MagicMock() + artifact.url = AI_PREFIX + "onlysecret" # no slash + self.mock_ai_core_client.artifact.get.return_value = artifact + mock_fetch_secret.return_value = None + + aws_creds = _AWSObjectStoreData(aws_access_key_id="a", aws_secret_access_key="b") + src = ArtifactSource(artifact="id", path=None, file_type="csv") + out = resolve_artifact_path(src, self.mock_ai_core_client, aws_creds, "rg", self.collector) + self.assertEqual(out, []) + + +class TestFetchDeploymentConfig(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.deployment = MagicMock() + + def test_fetch_deployment_config_success(self): + self.mock_ai_core_client.deployment.get.return_value = "depconfig" + c = ValidationCollector() + out = fetch_deployment_config("did", self.mock_ai_core_client, "rg", c) + self.assertEqual(out, "depconfig") + + def test_fetch_deployment_config_failure(self): + self.mock_ai_core_client.deployment.get.side_effect = Exception("no") + c = ValidationCollector() + out = fetch_deployment_config("did", self.mock_ai_core_client, "rg", c) + self.assertEqual(out, []) + + +class TestFetchConfigurationById(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.configuration = MagicMock() + + def test_fetch_configuration_by_id_success(self): + self.mock_ai_core_client.configuration.get.return_value = "cfg" + c = ValidationCollector() + out = fetch_configuration_by_id("cfgid", self.mock_ai_core_client, "rg", c) + self.assertEqual(out, "cfg") + + def test_fetch_configuration_by_id_failure(self): + self.mock_ai_core_client.configuration.get.side_effect = Exception("fail") + c = ValidationCollector() + out = fetch_configuration_by_id("cfgid", self.mock_ai_core_client, "rg", c) + self.assertEqual(out, []) + + +class TestCallOrchestrationServiceWithV2Config(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.rest_client = MagicMock() + self.collector = ValidationCollector() + + def test_call_orchestration_service_with_v2_config_success(self): + # Test that no errors are added when the call succeeds + # We use a real simple test dict that should pass validation + test_config = { + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "1.0", "params": {}}, + "prompt": {"template": [{"role": "user", "content": "test"}]} + } + } + } + + # Mock the OrchestrationService where it's used (in aicore_utils) + with patch(f"{MODULE_PATH}.OrchestrationService") as mock_service_class: + mock_service = MagicMock() + mock_service.run.return_value = {"ok": True} + mock_service_class.return_value = mock_service + + call_orchestration_service_with_v2_config( + test_config, self.mock_ai_core_client, "http://or", "rg", self.collector + ) + + self.assertFalse(self.collector.errors) + + def test_call_orchestration_service_with_v2_config_failure(self): + # Test that errors are collected when the call fails + test_config = {"test": "config"} + + # Mock the OrchestrationService where it's used (in aicore_utils) + with patch(f"{MODULE_PATH}.OrchestrationService") as mock_service_class: + mock_service = MagicMock() + mock_service.run.side_effect = Exception("Service error") + mock_service_class.return_value = mock_service + + call_orchestration_service_with_v2_config( + test_config, self.mock_ai_core_client, "http://or", "rg", self.collector + ) + + self.assertTrue(self.collector.errors) + + def test_call_orchestration_service_with_proxy_client_provided(self): + # Test that when proxy_client is provided, it's passed to OrchestrationService + test_config = { + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "1.0", "params": {}}, + "prompt": {"template": [{"role": "user", "content": "test"}]} + } + } + } + + mock_proxy_client = MagicMock() + + with patch(f"{MODULE_PATH}.OrchestrationService") as mock_service_class: + mock_service = MagicMock() + mock_service.run.return_value = {"ok": True} + mock_service_class.return_value = mock_service + + call_orchestration_service_with_v2_config( + test_config, + self.mock_ai_core_client, + "http://or", + "rg", + self.collector, + proxy_client=mock_proxy_client + ) + + # Verify OrchestrationService was called with the proxy_client + mock_service_class.assert_called_once_with( + api_url="http://or", + proxy_client=mock_proxy_client + ) + self.assertFalse(self.collector.errors) + + def test_call_orchestration_service_with_proxy_client_none(self): + # Test that when proxy_client is None, it's still passed to OrchestrationService + # OrchestrationService will handle None by creating its own proxy client + test_config = { + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "1.0", "params": {}}, + "prompt": {"template": [{"role": "user", "content": "test"}]} + } + } + } + + with patch(f"{MODULE_PATH}.OrchestrationService") as mock_service_class: + mock_service = MagicMock() + mock_service.run.return_value = {"ok": True} + mock_service_class.return_value = mock_service + + call_orchestration_service_with_v2_config( + test_config, + self.mock_ai_core_client, + "http://or", + "rg", + self.collector, + proxy_client=None + ) + + # Verify OrchestrationService was called with proxy_client=None + mock_service_class.assert_called_once_with( + api_url="http://or", + proxy_client=None + ) + self.assertFalse(self.collector.errors) + + +class TestUploadFileToAwsS3(unittest.TestCase): + def setUp(self): + self.collector = ValidationCollector() + + @patch(f"{MODULE_PATH}.S3FileClient") + def test_upload_file_to_aws_s3_csv(self, mock_s3_client): + mock_boto = MagicMock() + mock_boto.upload_csv.return_value = "ok" + mock_s3_client.return_value = mock_boto + + aws_creds = _AWSObjectStoreData(aws_access_key_id="a", aws_secret_access_key="b") + meta = {AWS_OSS_BUCKET_URL_KEY: "b"} + out = upload_file_to_aws_s3(aws_creds, meta, [["r"]], "k", CSV, self.collector) + self.assertEqual(out, "ok") + + @patch(f"{MODULE_PATH}.S3FileClient") + def test_upload_file_to_aws_s3_jsonl(self, mock_s3_client): + mock_boto = MagicMock() + mock_boto.upload_jsonl.return_value = "ok" + mock_s3_client.return_value = mock_boto + aws_creds = _AWSObjectStoreData(aws_access_key_id="a", aws_secret_access_key="b") + out = upload_file_to_aws_s3(aws_creds, {}, {"k": "v"}, "k", "jsonl", self.collector) + self.assertEqual(out, "ok") + + +class TestUploadEvaluationDatasetData(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.collector = ValidationCollector() + + @patch(f"{MODULE_PATH}.upload_file_to_aws_s3") + @patch(f"{MODULE_PATH}.fetch_object_store_secret_by_name") + def test_upload_evaluation_dataset_data_success(self, mock_fetch_secret, mock_upload): + mock_secret = MagicMock() + mock_secret.metadata = {AWS_PATH_KEY: "prefix"} + mock_fetch_secret.return_value = mock_secret + mock_upload.return_value = True + + eval_data = _EvaluationConfigData( + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["m"], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=True, + orch_config_data={}, + metric_templates=[], + ) + + aws_creds = _AWSObjectStoreData( + aws_access_key_id="a", + aws_secret_access_key="b", + ) + + folder, path = upload_evaluation_dataset_data( + eval_data, + aws_creds, + "secret-name", + self.mock_ai_core_client, + "rg", + self.collector, + ) + + self.assertIsInstance(folder, str) + self.assertIsInstance(path, str) + self.assertTrue(path.startswith(DATASET_FOLDER_KEY) or path == "") + + @patch(f"{MODULE_PATH}.upload_file_to_aws_s3") + @patch(f"{MODULE_PATH}.fetch_object_store_secret_by_name") + def test_upload_evaluation_dataset_data_upload_failure(self, mock_fetch_secret, mock_upload): + mock_secret = MagicMock() + mock_secret.metadata = {} + mock_fetch_secret.return_value = mock_secret + mock_upload.return_value = False + + eval_data = _EvaluationConfigData( + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=True, + orch_config_data=DUMMY_ORCH_CONFIG, + metric_templates=DUMMY_METRIC_TEMPLATES + ) + + aws_creds = _AWSObjectStoreData(aws_access_key_id="a", aws_secret_access_key="b") + folder, path = upload_evaluation_dataset_data( + eval_data, aws_creds, "secret-name", self.mock_ai_core_client, "rg", self.collector + ) + + self.assertEqual(path, "") + + +class TestRegisterAicoreArtifact(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.artifact = MagicMock() + self.collector = ValidationCollector() + + def test_register_aicore_artifact_success(self): + self.mock_ai_core_client.artifact.create.return_value.id = "art-1" + res = register_aicore_artifact("folder", self.mock_ai_core_client, "rg", "secret", self.collector) + self.assertEqual(res, "art-1") + + def test_register_aicore_artifact_failure(self): + self.mock_ai_core_client.artifact.create.side_effect = Exception("no") + res = register_aicore_artifact("folder", self.mock_ai_core_client, "rg", "secret", self.collector) + self.assertEqual(res, "") + + +class TestRegisterAicoreConfiguration(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.configuration = MagicMock() + self.collector = ValidationCollector() + + def test_register_aicore_configuration_with_llm_and_template(self): + acc = _EvaluationConfigData( + dataset_data=None, + dataset_type="csv", + metrics_list=["m"], + variable_mapping={}, + tags={}, + test_row_count=1, + repetitions=1, + debug_mode=True, + orch_config_data=DUMMY_ORCH_CONFIG, + metric_templates=DUMMY_METRIC_TEMPLATES + ) + + self.mock_ai_core_client.configuration.create.return_value.id = "cfg-1" + + config_id = register_aicore_configuration( + aicore_artifact_id="aid", + ai_core_client=self.mock_ai_core_client, + resource_group="rg", + accumulated_config_data=acc, + orchestration_url="http://orch", + dataset_file_key="x.csv", + run_ids_list=["r1", "r2"], + llm_model_config="modelcfg", + template_config=["tpl"], + orchestration_registry_config=None, + error_collector=self.collector, + ) + + self.assertEqual(config_id, "cfg-1") + + def test_register_aicore_configuration_with_orch_registry(self): + acc = _EvaluationConfigData( + dataset_data=None, + dataset_type="csv", + metrics_list=["m"], + variable_mapping={}, + tags={}, + test_row_count=1, + repetitions=1, + debug_mode=False, + orch_config_data=DUMMY_ORCH_CONFIG, + metric_templates=DUMMY_METRIC_TEMPLATES + ) + + self.mock_ai_core_client.configuration.create.return_value.id = "cfg-2" + + config_id = register_aicore_configuration( + aicore_artifact_id="aid", + ai_core_client=self.mock_ai_core_client, + resource_group="rg", + accumulated_config_data=acc, + orchestration_url="http://orch", + dataset_file_key="x.csv", + run_ids_list=["r1"], + llm_model_config=None, + template_config=None, + orchestration_registry_config="orch-ids", + error_collector=self.collector, + ) + + self.assertEqual(config_id, "cfg-2") + + def test_register_aicore_configuration_failure(self): + self.mock_ai_core_client.configuration.create.side_effect = Exception("bad") + + acc = _EvaluationConfigData( + dataset_data=None, + dataset_type="csv", + metrics_list=["m"], + variable_mapping={}, + tags={}, + test_row_count=1, + repetitions=1, + debug_mode=True, + orch_config_data=DUMMY_ORCH_CONFIG, + metric_templates=DUMMY_METRIC_TEMPLATES + ) + + res = register_aicore_configuration( + aicore_artifact_id="aid", + ai_core_client=self.mock_ai_core_client, + resource_group="rg", + accumulated_config_data=acc, + orchestration_url="http://orch", + dataset_file_key="x.csv", + run_ids_list=["r1"], + llm_model_config="modelcfg", + template_config=["tpl"], + orchestration_registry_config=None, + error_collector=self.collector, + ) + + self.assertIsNone(res) + + +class TestRegisterAicoreExecution(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.execution = MagicMock() + self.collector = ValidationCollector() + + def test_register_aicore_execution_success(self): + self.mock_ai_core_client.execution.create.return_value.id = "exec-1" + exec_id = register_aicore_execution(self.mock_ai_core_client, "cfg", "rg", self.collector) + self.assertEqual(exec_id, "exec-1") + + def test_register_aicore_execution_failure(self): + self.mock_ai_core_client.execution.create.side_effect = Exception("err") + res = register_aicore_execution(self.mock_ai_core_client, "cfg", "rg", self.collector) + self.assertIsNone(res) + + +class TestListAvailableLlmModels(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.model = MagicMock() + + def test_list_available_llm_models_success(self): + self.mock_ai_core_client.model.query.return_value.resources = ["m1"] + out = list_available_llm_models(self.mock_ai_core_client, "rg") + self.assertEqual(out, ["m1"]) + + def test_list_available_llm_models_failure(self): + self.mock_ai_core_client.model.query.side_effect = Exception("err") + with self.assertRaises(RuntimeError): + list_available_llm_models(self.mock_ai_core_client, "rg") + + +class TestFetchOrchestrationConfigFromRegistry(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.rest_client = MagicMock() + self.collector = ValidationCollector() + + def test_fetch_orchestration_config_from_registry_ok(self): + self.mock_ai_core_client.rest_client.get.return_value = {"spec": {"one": 1}} + out = fetch_orchestration_config_from_registry("ref-1", self.mock_ai_core_client, self.collector) + self.assertEqual(out, {"one": 1}) + + def test_fetch_orchestration_config_from_registry_error(self): + self.mock_ai_core_client.rest_client.get.side_effect = Exception("bad") + out = fetch_orchestration_config_from_registry("ref-1", self.mock_ai_core_client, self.collector) + self.assertIsNone(out) + + +class TestResolveMetricIdentifiers(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.collector = ValidationCollector() + + @patch(f"{MODULE_PATH}.get_custom_metric_by_id") + @patch(f"{MODULE_PATH}.get_metric_version_history") + @patch(f"{MODULE_PATH}.get_metric_template_info_from_server") + def test_resolve_metric_identifiers_all_paths(self, mock_sys, mock_hist, mock_by_id): + mc_id = MetricConfig(reference=MetricRef(id="id-1")) + mc_hist = MetricConfig(reference=MetricRef(scenario="s", name="n", version="v")) + mc_sys = MetricConfig(reference=MetricRef(name=list(SYSTEM_DEFINED_METRIC_MAPPING.values())[0])) + + mock_by_id.return_value = {"id": "id-1"} + mock_hist.return_value = {"id": "hist-1"} + mock_sys.return_value = {"id": "sys-1"} + + out = resolve_metric_identifiers([mc_id, mc_hist, mc_sys], self.mock_ai_core_client, "rg", self.collector) + self.assertIsInstance(out, list) + self.assertEqual({o["id"] for o in out}, {"id-1", "hist-1", "sys-1"}) + + +class TestResolveMetricNames(unittest.TestCase): + def setUp(self): + self.collector = ValidationCollector() + + def test_resolve_metric_names_valid_and_invalid(self): + m1 = MetricConfig(reference=MetricRef(id="uuid-1")) + m2 = MetricConfig(reference=MetricRef(scenario="sc", name="n", version="1")) + m3 = MetricConfig(reference=MetricRef(name="metric-name")) + m4 = MetricConfig(reference=MetricRef()) + + res = resolve_metric_names([m1, m2, m3], self.collector) + self.assertEqual(res, ["uuid-1", "sc/n/1", "metric-name"]) + + c = ValidationCollector() + resolve_metric_names([m4], c) + self.assertTrue(c.errors) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/tests/evaluations/test_client.py b/packages/gen/tests/evaluations/test_client.py new file mode 100644 index 0000000..acaad9e --- /dev/null +++ b/packages/gen/tests/evaluations/test_client.py @@ -0,0 +1,520 @@ +import unittest +from unittest.mock import MagicMock, patch +from types import SimpleNamespace + +import gen_ai_hub.evaluations.client as module +from gen_ai_hub.evaluations.client import EvaluationClient, _has_mixed_config_types +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec, PromptTemplate +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.evaluations.constants import ( + DEFAULT_KEY, + ORCHESTRATION_URL_SETUP_KEY, + OBJECT_STORE_SECRET_EXISTS_MESSAGE, + INPUT_SECRET_SETUP_KEY, + DEFAULT_SECRET_SETUP_KEY, +) + + +class TestHasMixedConfigTypes(unittest.TestCase): + """Tests for _has_mixed_config_types helper function.""" + + def test_empty_list_returns_false(self): + """Test that an empty list returns False.""" + self.assertFalse(_has_mixed_config_types([])) + + def test_single_config_returns_false(self): + """Test that a single config returns False.""" + config = EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + self.assertFalse(_has_mixed_config_types([config])) + + def test_all_llm_configs_returns_false(self): + """Test that all llm configs return False.""" + configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + llm=LLM(name="gpt-3.5", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test2")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + self.assertFalse(_has_mixed_config_types(configs)) + + def test_all_registry_configs_returns_false(self): + """Test that all orchestration_registry configs return False.""" + configs = [ + EvaluationConfig( + orchestration_registry_reference="uuid1", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + orchestration_registry_reference="uuid2", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + self.assertFalse(_has_mixed_config_types(configs)) + + def test_mixed_configs_returns_true(self): + """Test that mixed llm and registry configs return True.""" + configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + orchestration_registry_reference="uuid1", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + self.assertTrue(_has_mixed_config_types(configs)) + + +class TestEvaluationClient(unittest.TestCase): + + def setUp(self): + # Prevent real get_proxy_client side-effects + patcher = patch( + "gen_ai_hub.evaluations.client.get_proxy_client", + autospec=True, + ) + self.addCleanup(patcher.stop) + self.mock_proxy = patcher.start() + + self.fake_ai_core = MagicMock() + + def test_init_with_client_passed_and_attrs_set(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + c = EvaluationClient( + base_url="https://a", + resource_group="rg", + aws_access_key_id="AK", + aws_secret_access_key="SK", + ai_core_client=self.fake_ai_core, + ) + + self.assertEqual(c.base_url, "https://a") + self.assertEqual(c.resource_group, "rg") + self.assertIs(c.ai_core_client, self.fake_ai_core) + self.assertEqual(c.aws_access_key_id, "AK") + self.assertEqual(c.aws_secret_access_key, "SK") + + def test_init_missing_aws_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + with self.assertRaises(ValueError): + EvaluationClient( + base_url="x", + resource_group="rg", + ai_core_client=self.fake_ai_core, + ) + + def test_from_env_uses_fetch_credentials_and_constructs(self): + fake_creds = { + "base_url": "https://bv", + "resource_group": "rg", + "aws_access_key_id": "A", + "aws_secret_access_key": "B", + } + + with patch.object(module, "fetch_credentials", return_value=fake_creds), \ + patch("gen_ai_hub.evaluations.client.AICoreV2Client", autospec=True), \ + patch("gen_ai_hub.evaluations.client.get_proxy_client"): + + ec = EvaluationClient.from_env(profile_name="p") + + self.assertIsInstance(ec, EvaluationClient) + self.assertEqual(ec.base_url, "https://bv") + self.assertEqual(ec.resource_group, "rg") + self.assertEqual(ec.aws_access_key_id, "A") + + def test_setup_input_secret_name_missing_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + with self.assertRaises(ValueError): + client.setup( + input_secret_body={"type": "aws"}, + default_secret_body=None, + ) + + def test_setup_invalid_secret_type_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + bad_body = {"name": "n", "type": "unsupported-type"} + + with self.assertRaises(ValueError): + client.setup(input_secret_body=bad_body) + + def test_setup_secret_exists_replace_false_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + resp_exists = SimpleNamespace( + message=OBJECT_STORE_SECRET_EXISTS_MESSAGE + ) + + with patch( + "gen_ai_hub.evaluations.client.create_aws_object_store_secret", + return_value=resp_exists, + ): + with self.assertRaises(ValueError): + client.setup( + input_secret_body={"name": "in", "type": "aws"}, + replace_existing=False, + ) + + def test_setup_existing_config_and_running_deployment_reused(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group=DEFAULT_KEY, + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + cfg = MagicMock(id="cfg-1") + deployment = SimpleNamespace(deployment_url="https://running") + + with patch( + "gen_ai_hub.evaluations.client.get_orchestration_api_url", + return_value="https://running", + ): + + res = client.setup() + + self.assertEqual( + res[ORCHESTRATION_URL_SETUP_KEY], + "https://running", + ) + self.assertEqual(client.orchestration_url, "https://running") + + def test_setup_existing_config_no_running_deployment_creates_new(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group=DEFAULT_KEY, + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + cfg = MagicMock(id="cfg-1") + + with patch( + "gen_ai_hub.evaluations.client.get_orchestration_api_url", + side_effect=ValueError("No deployment found"), + ), patch( + "gen_ai_hub.evaluations.client.create_llm_orchestration_deployment_url", + return_value="https://new-orch", + ): + + res = client.setup() + + self.assertEqual( + res[ORCHESTRATION_URL_SETUP_KEY], + "https://new-orch", + ) + self.assertEqual(client.orchestration_url, "https://new-orch") + + def test_repr(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + repr_str = repr(client) + self.assertIn("EvaluationClient", repr_str) + self.assertIn("base_url", repr_str) + + def test_from_env_with_cert_url_conversion(self): + fake_creds = { + "base_url": "https://bv", + "resource_group": "rg", + "aws_access_key_id": "A", + "aws_secret_access_key": "B", + "cert_url": "https://cert-url", + } + + with patch.object(module, "fetch_credentials", return_value=fake_creds), \ + patch("gen_ai_hub.evaluations.client.AICoreV2Client", autospec=True), \ + patch("gen_ai_hub.evaluations.client.get_proxy_client"): + + ec = EvaluationClient.from_env(profile_name="p") + + self.assertIsInstance(ec, EvaluationClient) + # cert_url should have been converted to auth_url + self.assertNotIn("cert_url", fake_creds) + self.assertIn("auth_url", fake_creds) + + def test_setup_with_default_secret_body(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group=DEFAULT_KEY, + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + resp_success = SimpleNamespace(message="success") + + with patch( + "gen_ai_hub.evaluations.client.create_aws_object_store_secret", + return_value=resp_success, + ), patch( + "gen_ai_hub.evaluations.client.get_orchestration_api_url", + return_value="https://running", + ): + res = client.setup( + default_secret_body={"name": "default", "type": "S3"} + ) + + self.assertIn(DEFAULT_SECRET_SETUP_KEY, res) + self.assertEqual(client.default_object_store_secret_name, "default") + + def test_setup_with_user_provided_orchestration_url(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + orchestration_url="https://user-provided", + ) + + res = client.setup() + + self.assertEqual(res[ORCHESTRATION_URL_SETUP_KEY], "https://user-provided") + self.assertEqual(client.orchestration_url, "https://user-provided") + + def test_setup_with_input_secret_body_success(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + resp_success = SimpleNamespace(message="created") + + with patch( + "gen_ai_hub.evaluations.client.create_aws_object_store_secret", + return_value=resp_success, + ), patch( + "gen_ai_hub.evaluations.client.create_llm_orchestration_deployment_url", + return_value="https://new-deployment", + ): + res = client.setup( + input_secret_body={"name": "in", "type": "S3"}, + ) + + self.assertIn(INPUT_SECRET_SETUP_KEY, res) + self.assertEqual(client.input_object_store_secret_name, "in") + + def test_setup_secret_creation_fails_raises_runtime_error(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + with patch( + "gen_ai_hub.evaluations.client.create_aws_object_store_secret", + side_effect=Exception("API Error"), + ): + with self.assertRaises(RuntimeError) as context: + client.setup( + input_secret_body={"name": "in", "type": "S3"}, + ) + + self.assertIn("Creation of in object store secret failed", str(context.exception)) + + def test_setup_secret_exists_and_gets_replaced(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + resp_exists = SimpleNamespace(message=OBJECT_STORE_SECRET_EXISTS_MESSAGE) + resp_success = SimpleNamespace(message="created") + + # Use a list to track calls and return different results + responses = iter([resp_exists, resp_success]) + + with patch( + "gen_ai_hub.evaluations.client.create_aws_object_store_secret", + side_effect=lambda *args, **kwargs: next(responses), + ), patch( + "gen_ai_hub.evaluations.client.delete_object_store_secret" + ) as mock_delete, patch( + "gen_ai_hub.evaluations.client.create_llm_orchestration_deployment_url", + return_value="https://new-deployment", + ): + res = client.setup( + input_secret_body={"name": "in", "type": "S3"}, + replace_existing=True, + ) + + self.assertIn(INPUT_SECRET_SETUP_KEY, res) + mock_delete.assert_called_once() # Verify delete was called + + def test_evaluate_missing_default_secret_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + mock_config = MagicMock() + + with patch( + "gen_ai_hub.evaluations.client.fetch_object_store_secret_by_name", + return_value=None, + ): + with self.assertRaises(RuntimeError): + client.evaluate([mock_config]) + + def test_evaluate_missing_orchestration_url_raises(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + client.default_object_store_secret_name = "default" + + mock_config = MagicMock() + + with patch( + "gen_ai_hub.evaluations.client.fetch_object_store_secret_by_name", + return_value=MagicMock(), + ): + with self.assertRaises(RuntimeError): + client.evaluate([mock_config]) + + def test_resolve_orchestration_deployment_url_non_default_resource_group(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="non-default", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + with patch( + "gen_ai_hub.evaluations.client.create_llm_orchestration_deployment_url", + return_value="https://new-deployment", + ): + url = client.resolve_orchestration_deployment_url() + + self.assertEqual(url, "https://new-deployment") + + def test_list_available_models(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + mock_model = SimpleNamespace( + model="gpt-4", + provider="openai", + allowed_scenarios=[{"scenario_id": "orchestration"}], + versions=[SimpleNamespace(version="1.0", name="gpt-4")] + ) + + with patch( + "gen_ai_hub.evaluations.client.list_available_llm_models", + return_value=[mock_model], + ): + models = client.list_available_models() + + self.assertEqual(len(models), 1) + self.assertEqual(models[0]["model"], "gpt-4") + self.assertEqual(models[0]["provider"], "openai") + + def test_get_system_supported_metrics(self): + with patch("gen_ai_hub.evaluations.client.get_proxy_client"): + client = EvaluationClient( + "u", + resource_group="rg", + aws_access_key_id="A", + aws_secret_access_key="B", + ai_core_client=self.fake_ai_core, + ) + + with patch( + "gen_ai_hub.evaluations.client.fetch_all_system_predefined_metrics", + return_value=["metric1", "metric2"], + ): + metrics = client.get_system_supported_metrics() + + self.assertEqual(metrics, ["metric1", "metric2"]) \ No newline at end of file diff --git a/packages/gen/tests/evaluations/test_config_data.py b/packages/gen/tests/evaluations/test_config_data.py new file mode 100644 index 0000000..4ba69b8 --- /dev/null +++ b/packages/gen/tests/evaluations/test_config_data.py @@ -0,0 +1,342 @@ +import unittest +from unittest.mock import MagicMock + +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec, PromptTemplate +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData +from gen_ai_hub.evaluations.helpers.config_data import build_accumulated_config + + +class TestBuildAccumulatedConfig(unittest.TestCase): + """Tests for build_accumulated_config function.""" + + def test_single_execution_flow_with_same_dataset_and_metrics(self): + """Test that configs with same dataset and metrics use single execution flow.""" + # Create evaluation config data with same dataset and metrics + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-4"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-3.5"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + # Create evaluation configs (all with llm, no orchestration_registry_reference) + eval_configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + llm=LLM(name="gpt-3.5", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data, has_mixed_config_types=False + ) + + # Should use single execution flow + self.assertTrue(single_exec) + self.assertFalse(reusable) + # Should return accumulated config, not list + self.assertIsInstance(accumulated, _EvaluationConfigData) + + def test_multiple_execution_flow_with_different_datasets(self): + """Test that configs with different datasets use multiple execution flow.""" + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-4"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-3.5"}], + dataset_data=[{"x": 2}], # Different dataset + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + eval_configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test1.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + llm=LLM(name="gpt-3.5", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test2.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data, has_mixed_config_types=False + ) + + # Should NOT use single execution flow + self.assertFalse(single_exec) + self.assertFalse(reusable) + # Should return list of configs + self.assertIsInstance(accumulated, list) + + def test_mixed_config_types_prevents_single_execution(self): + """Test that mixing llm+template and orchestration_registry configs prevents single execution.""" + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-4"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"registry": "some-uuid"}], + dataset_data=[{"x": 1}], # Same dataset + dataset_type="csv", + metrics_list=["metric1"], # Same metrics + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + # Mixed configs: one with llm, one with orchestration_registry_reference + eval_configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + orchestration_registry_reference="some-uuid", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data, has_mixed_config_types=True # Mixed: llm + registry + ) + + # Should NOT use single execution flow due to mixed types + self.assertFalse(single_exec) + # Should enable artifact reuse (same dataset, even though mixed types) + self.assertTrue(reusable) + # Should return list of configs + self.assertIsInstance(accumulated, list) + + def test_all_orchestration_registry_configs_allow_single_execution(self): + """Test that all orchestration_registry configs can use single execution.""" + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"registry": "uuid1"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"registry": "uuid2"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + # All configs with orchestration_registry_reference + eval_configs = [ + EvaluationConfig( + orchestration_registry_reference="uuid1", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + orchestration_registry_reference="uuid2", + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ) + ] + + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data, has_mixed_config_types=False + ) + + # Should use single execution flow + self.assertTrue(single_exec) + self.assertFalse(reusable) + self.assertIsInstance(accumulated, _EvaluationConfigData) + + def test_reusable_artifact_with_same_dataset_different_metrics(self): + """Test that configs with same dataset but different metrics enable artifact reuse.""" + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-4"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-3.5"}], + dataset_data=[{"x": 1}], # Same dataset + dataset_type="csv", + metrics_list=["metric2"], # Different metrics + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + eval_configs = [ + EvaluationConfig( + llm=LLM(name="gpt-4", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric1"))] + ), + EvaluationConfig( + llm=LLM(name="gpt-3.5", version="1.0"), + template=PromptTemplateSpec( + template=[PromptTemplate(role="user", content="test")] + ), + dataset_config=Dataset("test.csv"), + metrics=[MetricConfig(reference=MetricRef(name="metric2"))] + ) + ] + + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data, has_mixed_config_types=False + ) + + # Should NOT use single execution (different metrics) + self.assertFalse(single_exec) + # Should enable artifact reuse (same dataset) + self.assertTrue(reusable) + self.assertIsInstance(accumulated, list) + + def test_backwards_compatibility_without_evaluation_configs(self): + """Test that function works without evaluation_configs parameter (backwards compatibility).""" + eval_config_data = [ + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-4"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ), + _EvaluationConfigData( + orch_config_data=[{"model": "gpt-3.5"}], + dataset_data=[{"x": 1}], + dataset_type="csv", + metrics_list=["metric1"], + metric_templates=[], + variable_mapping={}, + tags={}, + test_row_count=10, + repetitions=1, + debug_mode=False + ) + ] + + # Call without evaluation_configs + accumulated, single_exec, reusable = build_accumulated_config( + eval_config_data + ) + + # Should still work and use single execution (no mixed type detection) + self.assertTrue(single_exec) + self.assertFalse(reusable) + self.assertIsInstance(accumulated, _EvaluationConfigData) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/evaluations/test_config_data_utils.py b/packages/gen/tests/evaluations/test_config_data_utils.py new file mode 100644 index 0000000..cbb87b1 --- /dev/null +++ b/packages/gen/tests/evaluations/test_config_data_utils.py @@ -0,0 +1,355 @@ +import unittest +from unittest.mock import MagicMock, patch + +from ai_core_sdk.ai_core_v2_client import AICoreV2Client +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID, TemplateRefByScenarioNameVersion +from gen_ai_hub.prompt_registry.models.prompt_template import ( + PromptTemplateSpec, + PromptTemplate, +) + +from gen_ai_hub.evaluations.utils.config_data_utils import ( + _fetch_template_by_guid, + _register_prompt_template, + _get_prompt_template_uuid_by_metadata, + get_orch_config_data, + get_dataset_data, +) + +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode + + +class TestConfigDataUtils(unittest.TestCase): + + def test_fetch_template_by_guid_success(self): + collector = ValidationCollector() + client = MagicMock() + client.get_prompt_template_by_id.return_value.spec.template = ["template"] + + result = _fetch_template_by_guid(client, "uuid", collector) + + self.assertEqual(result, ["template"]) + + def test_fetch_template_by_guid_exception(self): + collector = ValidationCollector() + client = MagicMock() + client.get_prompt_template_by_id.side_effect = RuntimeError("boom") + + result = _fetch_template_by_guid(client, "uuid", collector) + + self.assertIsNone(result) + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.generate_random_id", + return_value="abcdef123" + ) + def test_register_prompt_template_with_string(self, _): + collector = ValidationCollector() + client = MagicMock() + client.create_prompt_template.return_value.id = "pt-123" + + template = [PromptTemplate(role="user", content="hello")] + + result = _register_prompt_template(template, client, collector) + + self.assertEqual(result, "pt-123") + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.generate_random_id", + return_value="abcdef123" + ) + def test_register_prompt_template_with_spec(self, _): + collector = ValidationCollector() + client = MagicMock() + client.create_prompt_template.return_value.id = "pt-456" + + spec = PromptTemplateSpec( + template=[PromptTemplate(role="user", content="hi")] + ) + + result = _register_prompt_template(spec, client, collector) + + self.assertEqual(result, "pt-456") + + def test_register_prompt_template_exception(self): + collector = ValidationCollector() + client = MagicMock() + client.create_prompt_template.side_effect = RuntimeError("fail") + + result = _register_prompt_template("hello", client, collector) + + self.assertIsNone(result) + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_get_prompt_template_uuid_by_metadata_success(self): + collector = ValidationCollector() + client = MagicMock() + + response = MagicMock() + response.resources = [MagicMock(id="uuid-123")] + client.get_prompt_templates.return_value = response + + template_ref = TemplateRef( + template_ref=TemplateRefByScenarioNameVersion( + scenario="s", + name="n", + version="v", + ) + ) + + result = _get_prompt_template_uuid_by_metadata( + client, + template_ref, + collector, + ) + + self.assertEqual(result, "uuid-123") + + def test_get_prompt_template_uuid_by_metadata_exception(self): + collector = ValidationCollector() + client = MagicMock() + client.get_prompt_templates.side_effect = RuntimeError("fail") + + template_ref = TemplateRef( + template_ref=TemplateRefByScenarioNameVersion( + scenario="s", + name="n", + version="v", + ) + ) + + _get_prompt_template_uuid_by_metadata(client, template_ref, collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.resolve_orchestration_config_v2", + return_value=["orch"] + ) + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.PromptTemplateClient" + ) + def test_get_orch_config_data_template_string(self, mock_pt_client, _): + collector = ValidationCollector() + ai_client = MagicMock(spec=AICoreV2Client) + proxy_client = MagicMock() + + mock_pt_client.return_value.create_prompt_template.return_value.id = "pt-123" + + evaluation_config = EvaluationConfig( + llm="gpt-4", + template="hello", + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + result = get_orch_config_data( + evaluation_config, + ai_client, + proxy_client, + collector, + ) + + self.assertEqual(result, ["orch"]) + + def test_get_orch_config_data_invalid_llm_template_combo(self): + collector = ValidationCollector() + + evaluation_config = EvaluationConfig( + llm="gpt-4", + template=None, + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + get_orch_config_data( + evaluation_config, + MagicMock(), + MagicMock(), + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.fetch_orchestration_config_from_registry", + return_value=["registry"] + ) + def test_get_orch_config_data_registry_reference(self, _): + collector = ValidationCollector() + + evaluation_config = EvaluationConfig( + orchestration_registry_reference="ref", + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + result = get_orch_config_data( + evaluation_config, + MagicMock(), + MagicMock(), + collector, + ) + + self.assertEqual(result, ["registry"]) + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.resolve_artifact_path", + return_value=["artifact"] + ) + def test_get_dataset_data_artifact_source(self, _): + collector = ValidationCollector() + + artifact_source = MagicMock(spec=ArtifactSource) + dataset = Dataset(source=artifact_source) + + result = get_dataset_data( + dataset, + MagicMock(), + {}, + "rg", + collector, + ) + + self.assertEqual(result, ["artifact"]) + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.load_config_file", + return_value=[{"row": 1}] + ) + def test_get_dataset_data_file_source(self, _): + collector = ValidationCollector() + + dataset = Dataset(source="file.json") + + result = get_dataset_data( + dataset, + MagicMock(), + {}, + "rg", + collector, + ) + + self.assertEqual(result, [{"row": 1}]) + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.resolve_orchestration_config_v2", + return_value=["orch-config"] + ) + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils._register_prompt_template", + return_value="pt-123" + ) + def test_get_orch_config_data_prompt_template_spec( + self, mock_register, mock_resolve + ): + collector = ValidationCollector() + + template_spec = PromptTemplateSpec( + template=[PromptTemplate(role="user", content="hello")] + ) + + evaluation_config = EvaluationConfig( + llm="gpt-4", + template=template_spec, + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + result = get_orch_config_data( + evaluation_config, + MagicMock(), + MagicMock(), + collector, + ) + + self.assertEqual(result, ["orch-config"]) + mock_register.assert_called_once() + mock_resolve.assert_called_once() + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.resolve_orchestration_config_v2", + return_value=["orch-config"] + ) + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils._fetch_template_by_guid", + return_value=[{"role": "user", "content": "hello"}] + ) + def test_get_orch_config_data_template_ref_with_id( + self, mock_fetch, mock_resolve + ): + collector = ValidationCollector() + + template_ref = TemplateRef(template_ref=TemplateRefByID(id="template-uuid")) + + evaluation_config = EvaluationConfig( + llm="gpt-4", + template=template_ref, + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + result = get_orch_config_data( + evaluation_config, + MagicMock(), + MagicMock(), + collector, + ) + + self.assertEqual(result, ["orch-config"]) + mock_fetch.assert_called_once() + mock_resolve.assert_called_once() + + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.PROMPT_TEMPLATE_METADATA_FIELDS", + ["non_existent_attr"] + ) + @patch( + "gen_ai_hub.evaluations.utils.config_data_utils.resolve_orchestration_config_v2", + return_value=[] + ) + def test_get_orch_config_data_invalid_template_ref(self, _): + collector = ValidationCollector() + + llm = MagicMock() + llm.name = "gpt-4" + + # Create a TemplateRef with an inner object that doesn't have the expected attributes + # Using model_construct to bypass Pydantic validation + mock_inner = MagicMock() + del mock_inner.id # Remove the id attribute + del mock_inner.non_existent_attr # Remove the patched metadata field + template_ref = TemplateRef.model_construct(template_ref=mock_inner) + + evaluation_config = EvaluationConfig( + llm=llm, + template=template_ref, + dataset_config=Dataset(source="file.json"), + metrics=[], + ) + + result = get_orch_config_data( + evaluation_config, + MagicMock(), + MagicMock(), + collector, + ) + + self.assertEqual(result, []) + + with self.assertRaises(RuntimeError) as exc: + collector.raise_if_errors() + + self.assertIn( + ErrorCode.INVALID_TEMPLATE_REFERENCE_KEY.name, + str(exc.exception) + ) diff --git a/packages/gen/tests/evaluations/test_credentials.py b/packages/gen/tests/evaluations/test_credentials.py new file mode 100644 index 0000000..3bec910 --- /dev/null +++ b/packages/gen/tests/evaluations/test_credentials.py @@ -0,0 +1,348 @@ +import unittest +from unittest.mock import patch +import json +import os +import tempfile + +from gen_ai_hub.evaluations.credentials import ( + get_home, + get_nested_value, + VCAPEnvironment, + Service, + fetch_credentials, + init_conf, + extract_credentials, + resolve_credentials, + resolve_resource_group, + validate_credentials, + Source, +) + + +class TestGetHome(unittest.TestCase): + def test_get_home_from_env(self): + with patch.dict(os.environ, {"AICORE_HOME": "/custom/home"}): + result = get_home() + self.assertEqual(result, "/custom/home") + + def test_get_home_default(self): + with patch.dict(os.environ, {}, clear=True): + result = get_home() + # Should return the default path (~/.aicore) + self.assertTrue(result.endswith(".aicore")) + + +class TestGetNestedValue(unittest.TestCase): + def test_get_nested_value_single_key(self): + data = {"key": "value"} + result = get_nested_value(data, ["key"]) + self.assertEqual(result, "value") + + def test_get_nested_value_nested_keys(self): + data = {"level1": {"level2": {"level3": "value"}}} + result = get_nested_value(data, ["level1", "level2", "level3"]) + self.assertEqual(result, "value") + + def test_get_nested_value_missing_key_raises(self): + data = {"key": "value"} + with self.assertRaises(KeyError): + get_nested_value(data, ["missing"]) + + +class TestVCAPEnvironment(unittest.TestCase): + def test_from_dict(self): + env_dict = { + "aicore": [ + {"label": "aicore", "name": "service1"}, + {"label": "aicore", "name": "service2"}, + ] + } + vcap = VCAPEnvironment.from_dict(env_dict) + self.assertEqual(len(vcap.services), 2) + + def test_from_env_empty(self): + with patch.dict(os.environ, {}, clear=True): + vcap = VCAPEnvironment.from_env() + self.assertEqual(len(vcap.services), 0) + + def test_from_env_with_data(self): + vcap_data = {"aicore": [{"label": "aicore", "name": "test-service"}]} + with patch.dict(os.environ, {"VCAP_SERVICES": json.dumps(vcap_data)}): + vcap = VCAPEnvironment.from_env() + self.assertEqual(len(vcap.services), 1) + + def test_getitem(self): + env_dict = {"aicore": [{"label": "aicore", "name": "service1"}]} + vcap = VCAPEnvironment.from_dict(env_dict) + service = vcap["aicore"] + self.assertEqual(service.label, "aicore") + + def test_get_service_exactly_one(self): + env_dict = {"aicore": [{"label": "aicore", "name": "service1"}]} + vcap = VCAPEnvironment.from_dict(env_dict) + service = vcap.get_service("aicore", exactly_one=True) + self.assertEqual(service.name, "service1") + + def test_get_service_not_found_raises(self): + vcap = VCAPEnvironment(services=[]) + with self.assertRaises(KeyError) as context: + vcap.get_service("missing") + self.assertIn("No service found with label", str(context.exception)) + + def test_get_service_by_name_exactly_one(self): + env_dict = {"aicore": [{"label": "aicore", "name": "service1"}]} + vcap = VCAPEnvironment.from_dict(env_dict) + service = vcap.get_service_by_name("service1", exactly_one=True) + self.assertEqual(service.name, "service1") + + def test_get_service_by_name_not_found_raises(self): + vcap = VCAPEnvironment(services=[]) + with self.assertRaises(KeyError) as context: + vcap.get_service_by_name("missing") + self.assertIn("No service found with name", str(context.exception)) + + def test_get_service_not_exactly_one_returns_list(self): + env_dict = { + "aicore": [ + {"label": "aicore", "name": "service1"}, + {"label": "aicore", "name": "service2"}, + ] + } + vcap = VCAPEnvironment.from_dict(env_dict) + services = vcap.get_service("aicore", exactly_one=False) + self.assertIsInstance(services, list) + self.assertEqual(len(services), 2) + + +class TestService(unittest.TestCase): + def test_label_property(self): + env = {"label": "aicore", "name": "test"} + service = Service(env) + self.assertEqual(service.label, "aicore") + + def test_name_property(self): + env = {"label": "aicore", "name": "test-service"} + service = Service(env) + self.assertEqual(service.name, "test-service") + + def test_getitem(self): + env = {"credentials": {"clientid": "test-id"}} + service = Service(env) + result = service["credentials.clientid"] + self.assertEqual(result, "test-id") + + def test_get_with_default(self): + env = {"key": "value"} + service = Service(env) + result = service.get("missing", default="default-value") + self.assertEqual(result, "default-value") + + def test_get_without_default_raises(self): + env = {"key": "value"} + service = Service(env) + with self.assertRaises(KeyError) as context: + service.get("missing") + self.assertIn("Key 'missing' not found", str(context.exception)) + + def test_get_with_list_key(self): + env = {"level1": {"level2": "value"}} + service = Service(env) + result = service.get(["level1", "level2"]) + self.assertEqual(result, "value") + + +class TestInitConf(unittest.TestCase): + def test_init_conf_no_profile_no_config_file(self): + with patch("pathlib.Path.exists", return_value=False), patch.dict( + os.environ, {}, clear=True + ): + result = init_conf() + self.assertEqual(result, {}) + + def test_init_conf_with_valid_config_file(self): + config_data = {"AICORE_BASE_URL": "https://test.com"} + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as temp_file: + json.dump(config_data, temp_file) + temp_path = temp_file.name + + try: + with patch.dict(os.environ, {"AICORE_CONFIG": temp_path}, clear=True): + result = init_conf() + self.assertEqual(result, config_data) + finally: + os.unlink(temp_path) + + def test_init_conf_with_invalid_json_raises(self): + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as temp_file: + temp_file.write("invalid json") + temp_path = temp_file.name + + try: + with patch.dict(os.environ, {"AICORE_CONFIG": temp_path}, clear=True): + with self.assertRaises(KeyError) as context: + init_conf() + self.assertIn("not a valid json file", str(context.exception)) + finally: + os.unlink(temp_path) + + def test_init_conf_with_profile_not_found_raises(self): + with patch("pathlib.Path.exists", return_value=False): + with self.assertRaises(FileNotFoundError) as context: + init_conf(profile="nonexistent") + self.assertIn("Unable to locate profile config file", str(context.exception)) + + +class TestExtractCredentials(unittest.TestCase): + def test_extract_credentials_success(self): + source = Source("test", lambda cv: "test-value" if cv.name == "client_id" else None) + credentials = extract_credentials(source) + self.assertIn("client_id", credentials) + self.assertEqual(credentials["client_id"], "test-value") + + def test_extract_credentials_with_transform(self): + source = Source("test", lambda cv: "https://test.com" if cv.name == "auth_url" else None) + credentials = extract_credentials(source) + # Should have auth_url with transform applied + if "auth_url" in credentials: + self.assertTrue(credentials["auth_url"].endswith("/oauth/token")) + + def test_extract_credentials_with_exclude(self): + source = Source("test", lambda cv: "value") + credentials = extract_credentials(source, exclude=["client_id"]) + self.assertNotIn("client_id", credentials) + + +class TestResolveCredentials(unittest.TestCase): + def test_resolve_credentials_from_first_source(self): + source1 = Source("kwargs", lambda cv: "value1" if cv.name == "client_id" else None) + source2 = Source("env", lambda cv: "value2") + + credentials = resolve_credentials([source1, source2]) + self.assertEqual(credentials["client_id"], "value1") + + def test_resolve_credentials_no_source_raises(self): + source = Source("empty", lambda cv: None) + + with self.assertRaises(ValueError) as context: + resolve_credentials([source]) + self.assertIn("No credentials found", str(context.exception)) + + +class TestResolveResourceGroup(unittest.TestCase): + def test_resolve_resource_group_found(self): + source = Source("test", lambda cv: "test-rg" if cv.name == "resource_group" else None) + + result = resolve_resource_group([source]) + self.assertEqual(result, "test-rg") + + def test_resolve_resource_group_not_found(self): + source = Source("test", lambda cv: None) + + result = resolve_resource_group([source]) + self.assertIsNone(result) + + +class TestValidateCredentials(unittest.TestCase): + def test_validate_credentials_with_client_secret_success(self): + credentials = { + "client_id": "test-id", + "client_secret": "test-secret", + "auth_url": "https://auth.com", + "base_url": "https://api.com/v2", + } + # Should not raise + validate_credentials(credentials) + + def test_validate_credentials_with_cert_files_success(self): + credentials = { + "client_id": "test-id", + "cert_file_path": "/path/to/cert", + "key_file_path": "/path/to/key", + "auth_url": "https://auth.com", + "base_url": "https://api.com/v2", + } + # Should not raise + validate_credentials(credentials) + + def test_validate_credentials_no_auth_method_raises(self): + credentials = { + "client_id": "test-id", + "auth_url": "https://auth.com", + "base_url": "https://api.com/v2", + } + with self.assertRaises(ValueError) as context: + validate_credentials(credentials) + self.assertIn("No authentication method found", str(context.exception)) + + def test_validate_credentials_multiple_auth_methods_raises(self): + credentials = { + "client_id": "test-id", + "client_secret": "secret", + "cert_file_path": "/path/to/cert", + "key_file_path": "/path/to/key", + "auth_url": "https://auth.com", + "base_url": "https://api.com/v2", + } + with self.assertRaises(ValueError) as context: + validate_credentials(credentials) + self.assertIn("Multiple authentication methods found", str(context.exception)) + + def test_validate_credentials_missing_base_fields_raises(self): + credentials = { + "client_secret": "secret", + } + with self.assertRaises(ValueError) as context: + validate_credentials(credentials) + self.assertIn("Missing required credentials", str(context.exception)) + + +class TestFetchCredentials(unittest.TestCase): + def test_fetch_credentials_from_kwargs(self): + result = fetch_credentials( + client_id="test-client", + client_secret="test-secret", + auth_url="https://auth.com/oauth/token", + base_url="https://api.com/v2", + ) + self.assertEqual(result["client_id"], "test-client") + self.assertEqual(result["client_secret"], "test-secret") + + def test_fetch_credentials_from_env(self): + with patch.dict( + os.environ, + { + "AICORE_CLIENT_ID": "env-client", + "AICORE_CLIENT_SECRET": "env-secret", + "AICORE_AUTH_URL": "https://auth.com/oauth/token", + "AICORE_BASE_URL": "https://api.com/v2", + }, + ), patch("gen_ai_hub.evaluations.credentials.init_conf", return_value={}): + result = fetch_credentials() + self.assertEqual(result["client_id"], "env-client") + self.assertEqual(result["client_secret"], "env-secret") + + def test_fetch_credentials_cert_url_becomes_auth_url(self): + with patch( + "gen_ai_hub.evaluations.credentials.init_conf", return_value={} + ), patch.dict( + os.environ, + { + "AICORE_CLIENT_ID": "test-id", + "AICORE_CERT_URL": "https://cert.com/oauth/token", + "AICORE_BASE_URL": "https://api.com/v2", + "AICORE_CERT_STR": "cert-content", + "AICORE_KEY_STR": "key-content", + }, + clear=True, + ): + result = fetch_credentials() + self.assertIn("auth_url", result) + self.assertNotIn("cert_url", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/tests/evaluations/test_evaluation_run.py b/packages/gen/tests/evaluations/test_evaluation_run.py new file mode 100644 index 0000000..9d4896d --- /dev/null +++ b/packages/gen/tests/evaluations/test_evaluation_run.py @@ -0,0 +1,471 @@ +import uuid +import unittest +from unittest.mock import MagicMock, patch + +import pandas as pd +from ai_api_client_sdk.models.status import Status + +from gen_ai_hub.evaluations.models.evaluation_run import ( + EvaluationRun, + Results, + ExecutionStatusDetails, + _RunContext, + configure_pandas_display, +) +from gen_ai_hub.evaluations._internal._models import _AWSObjectStoreData +from gen_ai_hub.evaluations.constants import ( + DEFAULT_KEY, + AWS_OSS_PATH_PREFIX_URL_KEY, + RESULTS_FILE_KEY, + AWS_OSS_BUCKET_URL_KEY, + AWS_OSS_REGION_URL_KEY, + COMPLETIONS_TABLE_KEY, + METRICS_TABLE_KEY, +) + + +class TestRunContext(unittest.TestCase): + + def test_run_context_init(self): + mock_client = MagicMock() + mock_credentials = MagicMock() + + context = _RunContext( + execution_id="exec-123", + configuration_id="config-456", + artifact_id="artifact-789", + ai_core_client=mock_client, + resource_group="rg-test", + object_store_credentials=mock_credentials, + metrics_list=["metric1", "metric2"], + cached_results_data={"data": "test"}, + ) + + self.assertEqual(context.execution_id, "exec-123") + self.assertEqual(context.configuration_id, "config-456") + self.assertEqual(context.artifact_id, "artifact-789") + self.assertIs(context.ai_core_client, mock_client) + self.assertEqual(context.resource_group, "rg-test") + self.assertIs(context.object_store_credentials, mock_credentials) + self.assertEqual(context.metrics_list, ["metric1", "metric2"]) + self.assertEqual(context.cached_results_data, {"data": "test"}) + + +class TestEvaluationRun(unittest.TestCase): + + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.execution = MagicMock() + self.mock_ai_core_client.object_store_secrets = MagicMock() + self.mock_ai_core_client.base_url = "https://api.example.com" + self.mock_ai_core_client.rest_client = MagicMock() + + self.mock_object_store_credentials = _AWSObjectStoreData( + aws_access_key_id="test-key", + aws_secret_access_key="test-secret", + ) + + self.evaluation_run = EvaluationRun( + run_id="run-123", + execution_id="exec-456", + ai_core_client=self.mock_ai_core_client, + configuration_id="config-789", + artifact_id="artifact-abc", + resource_group="rg-test", + object_store_credentials=self.mock_object_store_credentials, + metrics_list=["metric1", "metric2"], + ) + + def test_evaluation_run_init(self): + run = self.evaluation_run + + self.assertEqual(run.id, "run-123") + self.assertEqual(run.status, Status.UNKNOWN) + self.assertEqual(run._run_context.execution_id, "exec-456") + self.assertEqual(run._run_context.configuration_id, "config-789") + self.assertEqual(run._run_context.artifact_id, "artifact-abc") + self.assertEqual(run._run_context.resource_group, "rg-test") + self.assertEqual(run._run_context.metrics_list, ["metric1", "metric2"]) + + def test_set_cached_results_data(self): + test_data = {"completions": [], "metrics": []} + self.evaluation_run.set_cached_results_data(test_data) + self.assertEqual(self.evaluation_run._cached_results_data, test_data) + + def test_execution_status_fetcher(self): + mock_response = MagicMock() + self.mock_ai_core_client.execution.get.return_value = mock_response + + result = self.evaluation_run._execution_status_fetcher() + + self.mock_ai_core_client.execution.get.assert_called_once_with( + execution_id="exec-456", + resource_group="rg-test", + select="status", + ) + self.assertIs(result, mock_response) + + @patch("gen_ai_hub.evaluations.models.evaluation_run.wait_for_target_status") + def test_wait_for_completion(self, mock_wait): + mock_wait.return_value = None + self.evaluation_run.wait_for_completion(timeout=100) + + mock_wait.assert_called_once() + self.assertEqual(mock_wait.call_args[1]["target_status"], Status.COMPLETED) + self.assertEqual(mock_wait.call_args[1]["timeout"], 100) + + @patch("gen_ai_hub.evaluations.models.evaluation_run.wait_for_target_status") + def test_wait_for_completion_default_timeout(self, mock_wait): + mock_wait.return_value = None + self.evaluation_run.wait_for_completion() + + self.assertEqual(mock_wait.call_args[1]["timeout"], 3600) + + def test_get_current_status_success(self): + mock_response = MagicMock() + mock_response.status = Status.RUNNING + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + status = self.evaluation_run.get_current_status() + self.assertEqual(status, Status.RUNNING) + + def test_get_current_status_exception(self): + self.evaluation_run._execution_status_fetcher = MagicMock(side_effect=Exception("API Error")) + + with self.assertRaises(ValueError): + self.evaluation_run.get_current_status() + + def test_get_debug_info_without_status_details(self): + mock_response = MagicMock() + mock_response.status = Status.DEAD + mock_response.status_details = None + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + result = self.evaluation_run.get_debug_info() + + self.assertIsInstance(result, ExecutionStatusDetails) + self.assertEqual(result.status, Status.DEAD) + self.assertIn("No specific details found", result.details) + + def test_get_debug_logs(self): + mock_log_item1 = MagicMock() + mock_log_item1.__dict__ = {"message": "log1", "level": "INFO"} + mock_log_item2 = MagicMock() + mock_log_item2.__dict__ = {"message": "log2", "level": "ERROR"} + + mock_logs = MagicMock() + mock_logs.data.result = [mock_log_item1, mock_log_item2] + self.mock_ai_core_client.execution.query_logs.return_value = mock_logs + + result = self.evaluation_run.get_debug_logs() + + self.mock_ai_core_client.execution.query_logs.assert_called_once_with( + execution_id="exec-456" + ) + + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["message"], "log1") + self.assertEqual(result[1]["message"], "log2") + + def test_results_with_completed_status(self): + mock_response = MagicMock() + mock_response.status = Status.COMPLETED + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + result = self.evaluation_run.results() + + self.assertIsInstance(result, Results) + self.assertIs(result.run, self.evaluation_run) + + def test_results_with_running_status(self): + mock_response = MagicMock() + mock_response.status = Status.RUNNING + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + with self.assertRaises(ValueError): + self.evaluation_run.results() + + def test_results_with_other_status(self): + mock_response = MagicMock() + mock_response.status = Status.DEAD + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + with self.assertRaises(ValueError): + self.evaluation_run.results() + + def test_load_results_tables_with_cached_data(self): + cached_data = {"completions": [], "metrics": []} + self.evaluation_run._run_context.cached_results_data = cached_data + + result = self.evaluation_run.load_results_tables() + + self.assertEqual(result, cached_data) + self.mock_ai_core_client.object_store_secrets.get.assert_not_called() + + @patch("gen_ai_hub.evaluations.models.evaluation_run.S3FileClient") + def test_load_results_tables_from_s3(self, mock_s3_client_class): + self.evaluation_run._run_context.cached_results_data = None + + mock_secret_response = MagicMock() + mock_secret_response.metadata = { + AWS_OSS_PATH_PREFIX_URL_KEY: "prefix/path", + AWS_OSS_BUCKET_URL_KEY: "bucket-name", + AWS_OSS_REGION_URL_KEY: "us-east-1", + } + + self.mock_ai_core_client.object_store_secrets.get.return_value = mock_secret_response + + mock_s3_client = MagicMock() + mock_s3_client.get_sqlitedb_tables_data_from_s3.return_value = { + COMPLETIONS_TABLE_KEY: [{"id": 1}], + METRICS_TABLE_KEY: [{"id": 2}], + } + mock_s3_client_class.return_value = mock_s3_client + + result = self.evaluation_run.load_results_tables() + + self.mock_ai_core_client.object_store_secrets.get.assert_called_once_with( + name=DEFAULT_KEY, + resource_group="rg-test", + ) + + expected_key = f"prefix/path/exec-456/tmp/{RESULTS_FILE_KEY}" + + mock_s3_client.get_sqlitedb_tables_data_from_s3.assert_called_once_with( + expected_key, + [COMPLETIONS_TABLE_KEY, METRICS_TABLE_KEY], + ) + + self.assertIn(COMPLETIONS_TABLE_KEY, result) + self.assertIn(METRICS_TABLE_KEY, result) + + def test_load_results_tables_exception(self): + self.evaluation_run._run_context.cached_results_data = None + self.mock_ai_core_client.object_store_secrets.get.side_effect = Exception("S3 Error") + + with self.assertRaises(RuntimeError): + self.evaluation_run.load_results_tables() + + def test_results_with_completed_status(self): + mock_response = MagicMock() + mock_response.status = Status.COMPLETED + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + result = self.evaluation_run.results() + + self.assertIsInstance(result, Results) + self.assertIs(result.run, self.evaluation_run) + + def test_results_with_running_status(self): + mock_response = MagicMock() + mock_response.status = Status.RUNNING + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + with self.assertRaises(ValueError): + self.evaluation_run.results() + + def test_results_with_other_status(self): + mock_response = MagicMock() + mock_response.status = Status.DEAD + self.evaluation_run._execution_status_fetcher = MagicMock(return_value=mock_response) + + with self.assertRaises(ValueError): + self.evaluation_run.results() + + + +class TestConfigurePandasDisplay(unittest.TestCase): + + @patch("pandas.set_option") + def test_configure_pandas_display(self, mock_set_option): + configure_pandas_display() + + self.assertEqual(mock_set_option.call_count, 4) + + calls = [call.args for call in mock_set_option.call_args_list] + + self.assertIn(("display.max_columns", None), calls) + self.assertIn(("display.max_rows", None), calls) + self.assertIn(("display.max_colwidth", None), calls) + self.assertIn(("display.expand_frame_repr", False), calls) + + +class TestResults(unittest.TestCase): + + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.execution = MagicMock() + self.mock_ai_core_client.object_store_secrets = MagicMock() + self.mock_ai_core_client.base_url = "https://api.example.com" + self.mock_ai_core_client.rest_client = MagicMock() + self.mock_ai_core_client.rest_client.get_token = MagicMock(return_value="mock-token") + + self.mock_object_store_credentials = _AWSObjectStoreData( + aws_access_key_id="test-key", + aws_secret_access_key="test-secret", + ) + + self.evaluation_run = EvaluationRun( + run_id="run-123", + execution_id="exec-456", + ai_core_client=self.mock_ai_core_client, + resource_group="rg-test", + object_store_credentials=self.mock_object_store_credentials, + ) + + mock_response = MagicMock() + mock_response.status = Status.COMPLETED + self.evaluation_run._execution_status_fetcher = MagicMock( + return_value=mock_response + ) + + # Patch Tracking before creating Results + self.tracking_patcher = patch("gen_ai_hub.evaluations.models.evaluation_run.Tracking") + self.mock_tracking_class = self.tracking_patcher.start() + self.mock_tracking_instance = MagicMock() + self.mock_tracking_class.return_value = self.mock_tracking_instance + + self.results = Results(self.evaluation_run) + + def tearDown(self): + self.tracking_patcher.stop() + + @patch("gen_ai_hub.evaluations.models.evaluation_run.configure_pandas_display") + def test_results_init(self, mock_configure): + # Reset the mock to count calls from this test + self.mock_tracking_class.reset_mock() + + result = Results(self.evaluation_run) + + self.assertIs(result.run, self.evaluation_run) + self.assertIsNone(result._data_store) + self.assertIs(result._run_context, self.evaluation_run._run_context) + mock_configure.assert_called_once() + + # Verify Tracking client was created + self.mock_tracking_class.assert_called_once_with( + base_url=self.mock_ai_core_client.base_url, + token_creator=self.mock_ai_core_client.rest_client.get_token, + resource_group="rg-test", + ) + + def test_ensure_loaded(self): + test_data = { + COMPLETIONS_TABLE_KEY: [{"id": 1}], + METRICS_TABLE_KEY: [{"id": 2}], + } + + self.evaluation_run.load_results_tables = MagicMock(return_value=test_data) + self.evaluation_run.set_cached_results_data = MagicMock() + + self.results._ensure_loaded() + + self.assertEqual(self.results._data_store, test_data) + self.evaluation_run.set_cached_results_data.assert_called_once_with(test_data) + + def test_ensure_loaded_already_loaded(self): + existing_data = {"data": "exists"} + self.results._data_store = existing_data + + self.results._ensure_loaded() + + self.assertEqual(self.results._data_store, existing_data) + + def test_filter_by_run_id(self): + run_id_uuid = uuid.uuid4() + run_id_hex = run_id_uuid.hex + + data = [ + {"run_id": run_id_hex, "value": 1}, + {"run_id": run_id_hex, "value": 2}, + {"run_id": "other-run-id", "value": 3}, + ] + + filtered = self.results._filter_by_run_id(data, str(run_id_uuid)) + + self.assertEqual(len(filtered), 2) + self.assertTrue(all(row["run_id"] == run_id_hex for row in filtered)) + + def test_completions(self): + run_id_uuid = uuid.uuid4() + run_id_hex = run_id_uuid.hex + self.evaluation_run.id = str(run_id_uuid) + + test_data = { + COMPLETIONS_TABLE_KEY: [ + {"run_id": run_id_hex, "completion": "test1"}, + {"run_id": run_id_hex, "completion": "test2"}, + {"run_id": "other-id", "completion": "test3"}, + ], + METRICS_TABLE_KEY: [], + } + + self.evaluation_run.load_results_tables = MagicMock(return_value=test_data) + + df = self.results.completions() + + self.assertIsInstance(df, pd.DataFrame) + self.assertEqual(len(df), 2) + self.assertTrue(all(row["run_id"] == run_id_hex for row in df.to_dict("records"))) + + def test_completions_exception(self): + self.evaluation_run.load_results_tables = MagicMock( + side_effect=Exception("Load error") + ) + + with self.assertRaises(ValueError): + self.results.completions() + + def test_metrics(self): + run_id_uuid = uuid.uuid4() + run_id_hex = run_id_uuid.hex + self.evaluation_run.id = str(run_id_uuid) + + test_data = { + COMPLETIONS_TABLE_KEY: [], + METRICS_TABLE_KEY: [ + {"run_id": run_id_hex, "metric": "metric1", "value": 0.9}, + {"run_id": run_id_hex, "metric": "metric2", "value": 0.8}, + {"run_id": "other-id", "metric": "metric3", "value": 0.7}, + ], + } + + self.evaluation_run.load_results_tables = MagicMock(return_value=test_data) + + df = self.results.metrics() + + self.assertIsInstance(df, pd.DataFrame) + self.assertEqual(len(df), 2) + self.assertTrue(all(row["run_id"] == run_id_hex for row in df.to_dict("records"))) + + def test_metrics_exception(self): + self.evaluation_run.load_results_tables = MagicMock( + side_effect=Exception("Load error") + ) + + with self.assertRaises(ValueError): + self.results.metrics() + + def test_aggregations(self): + run_id_uuid = uuid.uuid4() + run_id_hex = run_id_uuid.hex + self.evaluation_run.id = str(run_id_uuid) + + self.mock_tracking_instance.query.return_value = {"aggregations": {"metric1": 0.9}} + + result = self.results.aggregations() + + self.mock_tracking_instance.query.assert_called_once_with( + execution_ids=[run_id_hex] + ) + + self.assertEqual(result, {"aggregations": {"metric1": 0.9}}) + + def test_aggregations_exception(self): + run_id_uuid = uuid.uuid4() + self.evaluation_run.id = str(run_id_uuid) + + self.mock_tracking_instance.query.side_effect = Exception("Network error") + + with self.assertRaises(ValueError): + self.results.aggregations() diff --git a/packages/gen/tests/evaluations/test_file_utils.py b/packages/gen/tests/evaluations/test_file_utils.py new file mode 100644 index 0000000..25b943c --- /dev/null +++ b/packages/gen/tests/evaluations/test_file_utils.py @@ -0,0 +1,154 @@ +import json +import unittest +import tempfile +from pathlib import Path +from unittest.mock import patch + +from gen_ai_hub.evaluations.utils.file_utils import ( + load_config_file, + read_local_csv_file, +) +from gen_ai_hub.evaluations.utils.aicore_utils import generate_random_id +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector + + +class TestFileUtils(unittest.TestCase): + + def write_file(self, tmp_path: Path, name: str, content: str): + file = tmp_path / name + file.write_text(content, encoding="utf-8") + return file + + def test_load_config_file_file_not_found(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + + result = load_config_file(tmp_path / "missing.json", collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "Config file not found"): + collector.raise_if_errors() + + def test_load_config_file_path_is_directory(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + + result = load_config_file(tmp_path, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "is not an actual file"): + collector.raise_if_errors() + + def test_load_config_file_unsupported_file_type(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.txt", "abc") + + result = load_config_file(file, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "not supported"): + collector.raise_if_errors() + + def test_load_config_file_valid_json(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + data = {"key": "value"} + file = self.write_file(tmp_path, "config.json", json.dumps(data)) + + result = load_config_file(file, collector) + + self.assertEqual(result, data) + + def test_load_config_file_invalid_json(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.json", "{invalid json}") + + result = load_config_file(file, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "Failed to parse config file"): + collector.raise_if_errors() + + def test_load_config_file_valid_jsonl(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + content = '{"a": 1}\n{"b": 2}\n' + file = self.write_file(tmp_path, "config.jsonl", content) + + result = load_config_file(file, collector) + + self.assertEqual(result, [{"a": 1}, {"b": 2}]) + + def test_load_config_file_valid_csv(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.csv", "col1,col2\n1,2\n3,4\n") + + result = load_config_file(file, collector) + + self.assertEqual( + result, + [ + {"col1": "1", "col2": "2"}, + {"col1": "3", "col2": "4"}, + ], + ) + + def test_generate_random_id_format(self): + value = generate_random_id() + + self.assertIsInstance(value, str) + self.assertEqual(len(value), 32) + int(value, 16) # should not raise + + def test_generate_random_id_uniqueness(self): + ids = {generate_random_id() for _ in range(100)} + self.assertEqual(len(ids), 100) + + def test_load_config_file_value_error(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.json", "{}") + + with patch("json.load", side_effect=ValueError("forced value error")): + result = load_config_file(file, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "Value error reading config file"): + collector.raise_if_errors() + + def test_load_config_file_generic_exception(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.json", "{}") + + with patch("json.load", side_effect=RuntimeError("boom")): + result = load_config_file(file, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "Reading config file .* failed with"): + collector.raise_if_errors() + + def test_read_local_csv_file_generic_exception(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + collector = ValidationCollector() + file = self.write_file(tmp_path, "config.csv", "a,b\n1,2") + + with patch("pandas.read_csv", side_effect=RuntimeError("unexpected error")): + result = read_local_csv_file(file, collector) + + self.assertEqual(result, []) + with self.assertRaisesRegex(RuntimeError, "Reading config file .* failed with"): + collector.raise_if_errors() diff --git a/packages/gen/tests/evaluations/test_flat_import_evaluation.py b/packages/gen/tests/evaluations/test_flat_import_evaluation.py new file mode 100644 index 0000000..5c7635e --- /dev/null +++ b/packages/gen/tests/evaluations/test_flat_import_evaluation.py @@ -0,0 +1,28 @@ +def test_flat_and_not_flat_import_evaluation(): + from gen_ai_hub.evaluations import EvaluationClient as evaluation_client_flat + from gen_ai_hub.evaluations.client import EvaluationClient as evaluation_client + assert evaluation_client_flat == evaluation_client + + from gen_ai_hub.evaluations import Dataset as dataset_flat + from gen_ai_hub.evaluations.models.dataset_config import Dataset as dataset + assert dataset_flat == dataset + + from gen_ai_hub.evaluations import MetricConfig as metric_config_flat + from gen_ai_hub.evaluations.models.metric_config import MetricConfig as metric_config + assert metric_config_flat == metric_config + + from gen_ai_hub.evaluations import MetricRef as metric_ref_flat + from gen_ai_hub.evaluations.models.metric_config import MetricRef as metric_ref + assert metric_ref_flat == metric_ref + + from gen_ai_hub.evaluations import EvaluationConfig as evaluation_config_flat + from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig as evaluation_config + assert evaluation_config_flat == evaluation_config + + from gen_ai_hub.evaluations import ArtifactSource as artifact_source_flat + from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource as artifact_source + assert artifact_source_flat == artifact_source + + from gen_ai_hub.evaluations import EvaluationRun as evaluation_run_flat + from gen_ai_hub.evaluations.models.evaluation_run import EvaluationRun as evaluation_run + assert evaluation_run_flat == evaluation_run diff --git a/packages/gen/tests/evaluations/test_gen_utils.py b/packages/gen/tests/evaluations/test_gen_utils.py new file mode 100755 index 0000000..ca224fc --- /dev/null +++ b/packages/gen/tests/evaluations/test_gen_utils.py @@ -0,0 +1,3281 @@ +import json +import unittest +from unittest.mock import patch, MagicMock + +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode + +from gen_ai_hub.evaluations.utils.gen_utils import ( + get_mapped_value_if_exists, + list_prompt_variables, + flatten_prompt_configuration, + get_prompt_variables_from_orch_config, + get_custom_metric_ids_from_input, + populate_dataset_data_if_single_schema_provided, + populate_dataset_data_if_single_reference_provided, + populate_dataset_data_if_individual_metric_reference_provided, + extract_dataset_columns, + create_custom_metric_name, + count_user_prompts_from_template_list, + select_model_details_randomly, + create_model_versions_map_from_orch_configs, + create_model_versions_map_from_configuration_param_bindings, + handle_missing_dependent_variables_in_dataset, + handle_reference_missing_rows, + parse_model_filter_list, + build_model_versions_map, + validate_metrics, + validate_variable_mapping_of_metrics, + validate_variable_mapping_of_prompts, + update_variable_mapping, + get_accumulated_config_data, + create_model_versions_map_from_custom_metric_config, + update_test_orch_config, + validate_metric_name, + check_if_metric_is_defined, + extract_metrics_variables, + validate_individual_custom_metrics, + handle_json_schema_match, + validate_language_code_and_data_population, + handle_language_match, + update_artifact_dict, + resolve_orchestration_config_v2, +) + +from gen_ai_hub.evaluations.utils.validation_utils import ( + validate_orchestration_url, + validate_orchestration_configuration, + validate_input_config, + extract_deployment_id, +) + +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData + +from gen_ai_hub.evaluations.constants import ( + AICORE_LLM_PROMPT_TEMPLATE_KEY, + ALL_METRICS_COLUMN_MAPPING_KEY, + JSON_SCHEMA_MATCH_METRIC_ID, + LANGUAGE_MATCH_METRIC_ID, + LANGUAGE_KEY, + MODEL_CONFIGURATION_KEY, + MODEL_NAME_KEY, + MODEL_VERSION_KEY, + LATEST_MODEL_VERSION_KEY, + MODULES_KEY, + PROMPT_TEMPLATING_KEY, + PROMPT_KEY, + TEMPLATE_KEY, + MODEL_KEY, +) + +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplate +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM + + +@patch.dict("os.environ", {"METRICS_DATA_PATH": "metrics_info.json"}) +class TestGetMappedValueIfExists(unittest.TestCase): + def test_mapping_key_exists_with_data_prefix(self): + key = "original_key" + mapping_key = "prompt/mapping_key" + variable_mapping = {"prompt/mapping_key": "data/mapped_field"} + dataset_rows = ["mapped_field"] + result = get_mapped_value_if_exists( + key, mapping_key, variable_mapping, dataset_rows + ) + self.assertEqual(result, "mapped_field") + + def test_mapping_key_exists_with_other_prefix(self): + key = "original_key" + mapping_key = "prompt/mapping_key" + variable_mapping = {"prompt/mapping_key": "other/mapped_field"} + dataset_rows = ["field"] + result = get_mapped_value_if_exists( + key, mapping_key, variable_mapping, dataset_rows + ) + self.assertEqual(result, "original_key") + + def test_mapping_key_does_not_exist(self): + key = "original_key" + mapping_key = "prompt/non_existent_key" + variable_mapping = {"prompt/mapping_key": "data/mapped_field"} + dataset_rows = ["field"] + result = get_mapped_value_if_exists( + key, mapping_key, variable_mapping, dataset_rows + ) + self.assertEqual(result, "original_key") + + def test_mapping_key_exist_but_column_does_not_exist(self): + key = "original_key" + mapping_key = "prompt/mapping_key" + variable_mapping = {"prompt/mapping_key": "data/mapped_field"} + dataset_rows = ["field"] + result = get_mapped_value_if_exists( + key, mapping_key, variable_mapping, dataset_rows + ) + self.assertEqual(result, "original_key") + + +class TestPopulateTemplateVarsDataIfSingleSchemaProvided(unittest.TestCase): + def test_json_schema_key_directly_present_in_dataset_partial(self): + collector = ValidationCollector() + template_vars_data = [ + { + "topic": "banana", + "json_schema": '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"source_langauge":{"type":"string"},"is_supported":{"type":"boolean"},"translated_text":{"type":"string"}},"required":["source_language","is_supported","translated_text"]}', + }, + {"topic": "apple"}, + ] + variable_mapping = {} + + populate_dataset_data_if_single_schema_provided( + template_vars_data, variable_mapping, collector + ) + + # needs to replicate the schema value using existing value + self.assertEqual( + template_vars_data[1]["json_schema"], + '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"source_langauge":{"type":"string"},"is_supported":{"type":"boolean"},"translated_text":{"type":"string"}},"required":["source_language","is_supported","translated_text"]}' + ) + + def test_json_schema_key_present_in_first_row(self): + collector = ValidationCollector() + template_vars_data = [ + { + "topic": "ml", + "json_schema_key": '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"source_langauge":{"type":"string"},"is_supported":{"type":"boolean"},"translated_text":{"type":"string"}},"required":["source_language","is_supported","translated_text"]}', + }, + {"topic": "ai"}, + {"topic": "gen_ai"}, + ] + variable_mapping = {"json_schema_match/json_schema": "data/json_schema_key"} + + populate_dataset_data_if_single_schema_provided( + template_vars_data, variable_mapping, collector + ) + + self.assertEqual( + template_vars_data[1]["json_schema_key"], + '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"source_langauge":{"type":"string"},"is_supported":{"type":"boolean"},"translated_text":{"type":"string"}},"required":["source_language","is_supported","translated_text"]}' + ) + self.assertEqual( + template_vars_data[2]["json_schema_key"], + '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"source_langauge":{"type":"string"},"is_supported":{"type":"boolean"},"translated_text":{"type":"string"}},"required":["source_language","is_supported","translated_text"]}' + ) + + +# Tests for parse_prompt_template_content and get_variable_value_from_orch_response removed +# as these functions don't exist in the current gen_utils.py + + +class TestListPromptVariables(unittest.TestCase): + def test_single_variable(self): + content = "This is a prompt with a single variable {{ ?var1 }}." + result = list_prompt_variables(content) + self.assertEqual(result, ["var1"]) + + def test_multiple_variables(self): + content = "This is a prompt with multiple variables {{?var1}} and {{?var2}}." + result = list_prompt_variables(content) + self.assertEqual(result, ["var1", "var2"]) + + def test_no_variables(self): + content = "This is a prompt with no variables." + result = list_prompt_variables(content) + self.assertEqual(result, []) + + def test_nested_variables(self): + content = "This is a prompt with nested variables {{?var1}} and {{?var2}} inside {{?var3}}." + result = list_prompt_variables(content) + self.assertEqual(result, ["var1", "var2", "var3"]) + + def test_empty_variables_throws_exception(self): + content = "This is a prompt with empty variables {{?}}." + with self.assertRaises(ValueError): + list_prompt_variables(content) + + def test_unsupported_named_variables_throws_exception(self): + content = "This is a prompt with empty variables {{?12topics}}." + with self.assertRaises(ValueError): + list_prompt_variables(content) + + +class TestFormatPromptConfiguration(unittest.TestCase): + def test_flat_dict(self): + config = {"scenario": "genai-evaluations", "metricName": "groundedness"} + expected = "scenario: genai-evaluations\nmetricName: groundedness" + self.assertEqual(flatten_prompt_configuration(config), expected) + + def test_nested_dict(self): + config = { + "scenario": "genai-evaluations", + "prompt_configuration": {"rating": "rating", "data_type": "numerical"}, + } + expected = "scenario: genai-evaluations\nprompt_configuration: rating: rating, data_type: numerical" + self.assertEqual(flatten_prompt_configuration(config), expected) + + def test_list_values(self): + config = { + "model_parameters": ["temperature", "max_tokens"], + "parameter_values": {"temperature": [0.7, 0.8]}, + } + expected = "model_parameters: temperature; max_tokens\nparameter_values: temperature: 0.7; 0.8" + self.assertEqual(flatten_prompt_configuration(config), expected) + + def test_empty_dict(self): + config = {} + expected = "" + self.assertEqual(flatten_prompt_configuration(config), expected) + + def test_mixed_nested(self): + config = { + "metricName": "groundedness", + "model_configuration": { + "models": ["gpt-4", "gpt-3.5"], + "parameters": {"temperature": 0.7, "max_tokens": 50}, + }, + } + expected = ( + "metricName: groundedness\n" + "model_configuration: models: gpt-4; gpt-3.5, parameters: temperature: 0.7, max_tokens: 50" + ) + self.assertEqual(flatten_prompt_configuration(config), expected) + + +class TestGetPromptVariablesFromOrchConfig(unittest.TestCase): + def test_empty_config(self): + orch_config = {"modules": {"prompt_templating": {"prompt": {"template": []}}}} + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, set()) + + def test_single_template_with_variables(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + } + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2"}) + + def test_multiple_templates_with_variables(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "This is a prompt with {{?var1}}.", + }, + { + "role": "user", + "content": "This is another prompt with {{?var2}} and {{?var3}}.", + }, + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2", "var3"}) + + def test_output_param_removal(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "Prompt with {{?var1}} and {{?groundingOutput}}.", + }, + { + "role": "user", + "content": "Another prompt with {{?var2}}.", + }, + ] + } + }, + "grounding": { + "placeholders": { + "output": "groundingOutput" + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2"}) # 'groundingOutput' is removed + + def test_templates_with_no_variables(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "This is a prompt with no variables.", + }, + { + "role": "user", + "content": "Another prompt with no variables.", + }, + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, set()) + + def test_mixed_templates(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "This is a prompt with {{?var1}}.", + }, + { + "role": "user", + "content": "This is a prompt with no variables.", + }, + { + "role": "user", + "content": "Another prompt with {{?var2}}.", + }, + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2"}) + + +class TestGetMetrics(unittest.TestCase): + def test_get_custom_metric_ids_from_input(self): + collector = ValidationCollector() + custom_metric_config_data = [ + {"metricId": "custom_metric_1"}, + {"metricId": "custom_metric_2"}, + ] + custom_metric_ids = get_custom_metric_ids_from_input(custom_metric_config_data, collector) + self.assertEqual(len(custom_metric_ids), 2) + self.assertIn("custom_metric_1", custom_metric_ids) + self.assertIn("custom_metric_2", custom_metric_ids) + + def test_get_custom_metric_ids_from_input_if_empty(self): + collector = ValidationCollector() + custom_metric_config_data = [] + custom_metric_ids = get_custom_metric_ids_from_input(custom_metric_config_data, collector) + self.assertEqual(len(custom_metric_ids), 0) + + +class TestValidateMetrics(unittest.TestCase): + def test_metrics_empty_list(self): + collector = ValidationCollector() + metrics = [] + metrics_templates = [] + orchestration_config_data = [{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}] + with self.assertRaises(RuntimeError) as cm: + validate_metrics(metrics, metrics_templates, orchestration_config_data, collector) + collector.raise_if_errors() + self.assertIn("Metrics list cannot be empty. Atleast one metric needs to be provided", str(cm.exception)) + + def test_metrics_not_empty_but_one_metric_is_empty_string(self): + # can occur in case of missed syntax --> metrics = blue,,bertscore + collector = ValidationCollector() + metrics = ["bleu", "", "bert_score"] + metric_templates = [] + orchestration_config_data = [{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}] + with self.assertRaises(RuntimeError) as cm: + validate_metrics(metrics, metric_templates, orchestration_config_data, collector) + collector.raise_if_errors() + self.assertIn("Metric name cannot be empty. Please provide a valid metric name", str(cm.exception)) + + def test_metrics_list_with_empty_metric_name(self): + collector = ValidationCollector() + metrics = [""] + metric_templates = [] + orchestration_config_data = [{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}] + with self.assertRaises(RuntimeError) as cm: + validate_metrics(metrics, metric_templates, orchestration_config_data, collector) + collector.raise_if_errors() + self.assertIn("Metric name cannot be empty. Please provide a valid metric name", str(cm.exception)) + + def test_metrics_valid_system_supported_metrics(self): + collector = ValidationCollector() + metrics = ["bert_score", "bleu"] + metrics_templates = [ + { + "evaluationMethod": "computed", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "bert_score", + "name": "BERT Score", + "description": "This is a description for bert score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [0, 1], + "experimental": False, + }, + }, + { + "evaluationMethod": "computed", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "bleu", + "name": "BLEU", + "description": "This is a description for bleu score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [0, 1], + "experimental": False, + }, + }, + ] + orchestration_config_data = [{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}] + validate_metrics(metrics, metrics_templates, orchestration_config_data, collector) + collector.raise_if_errors() + + def test_metrics_llm_based_metric_more_than_one_user_prompts(self): + collector = ValidationCollector() + metrics = ["pointwise_correctness"] + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "This is a prompt with {{?var1}}.", + }, + { + "role": "user", + "content": "This is a prompt with no variables.", + }, + { + "role": "user", + "content": "Another prompt with {{?var2}}.", + }, + ] + } + } + } + } + + metrics_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "pointwise_correctness", + "name": "Pointwise Correctness", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + + orchestration_config_data = [orch_config] + with self.assertRaises(RuntimeError) as cm: + validate_metrics(metrics, metrics_templates, orchestration_config_data, collector) + collector.raise_if_errors() + self.assertIn("More than one user prompts provided in template list", str(cm.exception)) + + def test_metrics_llm_based_metric_missing_user_prompts(self): + collector = ValidationCollector() + metrics = ["pointwise_correctness"] + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "system", + "content": "This is a system prompt with no variables.", + } + ] + } + } + } + } + + metrics_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "pointwise_correctness", + "name": "Pointwise Correctness", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + + orchestration_config_data = [orch_config] + with self.assertRaises(RuntimeError) as cm: + validate_metrics(metrics, metrics_templates, orchestration_config_data, collector) + collector.raise_if_errors() + self.assertIn("Missing user prompt in template list", str(cm.exception)) + + +class TestValidateOrchesrationUrl(unittest.TestCase): + run1 = MagicMock() + run1.name = "run1" + run1.config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "system", + "content": "This is a system prompt with no variables.", + } + ] + }, + "model": {"name": "gpt-4", "version": "4.0"}, + } + } + } + run2 = MagicMock() + run2.name = "run2" + run2.config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "system", + "content": "This is a system prompt with no variables.", + } + ] + }, + "model": {"name": "llama2", "version": "7b"}, + } + } + } + run_data = [run1, run2] + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.gen_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.gen_utils.update_test_orch_config") + def test_valid_orch_url( + self, + mock_update_test_orch_config, + mock_select_model, + mock_fetch_and_validate, + mock_fetch_deployment_config, + mock_call_orchestration_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "config-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_fetch_and_validate.return_value = None + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_orch_config.return_value = {"test": "config"} + mock_call_orchestration_service.return_value = None + + test_orch_url = "https://api.ai.staging.eu-west-1.mlf-aws-dev.com/v2/inference/deployments/valid/" + config_data = _EvaluationConfigData( + orch_config_data=[self.run1.config], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + try: + validate_orchestration_url( + config_data, test_orch_url, mock_ai_core_client, "resource-group", collector + ) + collector.raise_if_errors() + except RuntimeError: + self.fail( + "validate_orchestration_url raised RuntimeError unexpectedly!" + ) + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.gen_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.gen_utils.update_test_orch_config") + def test_invalid_orch_url_raises_validation_error( + self, + mock_update_test_orch_config, + mock_select_model, + mock_fetch_and_validate, + mock_fetch_deployment_config, + mock_call_orchestration_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "config-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_fetch_and_validate.return_value = None + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_orch_config.return_value = {"test": "config"} + # Mock the function to add error to collector instead of raising + def mock_call_with_error(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get('error_collector') + if error_collector: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Invalid URL while trying to run the test orchestration config endpoint call with this user provided deployment url" + ) + mock_call_orchestration_service.side_effect = mock_call_with_error + + test_orch_url = "https://api.ai.staging.eu-west-1.mlf-aws-dev.com/v2/inference/deployments/invalid/" + config_data = _EvaluationConfigData( + orch_config_data=[self.run1.config], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + validate_orchestration_url( + config_data, test_orch_url, mock_ai_core_client, "resource-group", collector + ) + # Exception is caught and added to collector + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "Error occurred.*while trying to run the test orchestration config") + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.gen_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.gen_utils.update_test_orch_config") + def test_json_decode_error_handled_as_success( + self, + mock_update_test_orch_config, + mock_select_model, + mock_fetch_and_validate, + mock_fetch_deployment_config, + mock_call_orchestration_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "config-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_fetch_and_validate.return_value = None + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_orch_config.return_value = {"test": "config"} + # JSONDecodeError is caught internally and added to collector + def mock_call_with_json_error(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get('error_collector') + if error_collector: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Expecting value while trying to run the test orchestration config endpoint call" + ) + mock_call_orchestration_service.side_effect = mock_call_with_json_error + + test_orch_url = "https://api.ai.staging.eu-west-1.mlf-aws-dev.com/v2/inference/deployments/json-decode/" + config_data = _EvaluationConfigData( + orch_config_data=[self.run1.config], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + validate_orchestration_url( + config_data, test_orch_url, mock_ai_core_client, "resource-group", collector + ) + # Should have error in collector + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.gen_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.gen_utils.update_test_orch_config") + def test_retry_exception_handled_as_success( + self, + mock_update_test_orch_config, + mock_select_model, + mock_fetch_and_validate, + mock_fetch_deployment_config, + mock_call_orchestration_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "config-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_fetch_and_validate.return_value = None + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_orch_config.return_value = {"test": "config"} + # Exception is caught internally and added to collector + def mock_call_with_retry_error(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get('error_collector') + if error_collector: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Retry error while trying to run the test orchestration config endpoint call" + ) + mock_call_orchestration_service.side_effect = mock_call_with_retry_error + + test_orch_url = "https://api.ai.staging.eu-west-1.mlf-aws-dev.com/v2/inference/deployments/retry/" + config_data = _EvaluationConfigData( + orch_config_data=[self.run1.config], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + validate_orchestration_url( + config_data, test_orch_url, mock_ai_core_client, "resource-group", collector + ) + # Should have error in collector + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.gen_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.gen_utils.update_test_orch_config") + def test_unexpected_exception_raises_validation_error( + self, + mock_update_test_orch_config, + mock_select_model, + mock_fetch_and_validate, + mock_fetch_deployment_config, + mock_call_orchestration_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "config-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_fetch_and_validate.return_value = None + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_orch_config.return_value = {"test": "config"} + # ClientConnectionError is caught internally and added to collector + def mock_call_with_connection_error(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get('error_collector') + if error_collector: + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Unexpected while trying to run the test orchestration config endpoint call" + ) + mock_call_orchestration_service.side_effect = mock_call_with_connection_error + + test_orch_url = "https://api.ai.staging.eu-west-1.mlf-aws-dev.com/v2/inference/deployments/unexpected/" + config_data = _EvaluationConfigData( + orch_config_data=[self.run1.config], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + validate_orchestration_url( + config_data, test_orch_url, mock_ai_core_client, "resource-group", collector + ) + # Should have error in collector + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "Error occurred.*while trying to run") + + +class TestValidateOrchestrationConfiguration(unittest.TestCase): + def test_missing_modules(self): + collector = ValidationCollector() + run_data = [{}] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertIn("modules is mandatory in the orchestration config", str(cm.exception)) + + def test_missing_model_or_prompt_templating(self): + collector = ValidationCollector() + run_data = [{"modules": {}}] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertIn("prompt_templating is mandatory in the modules field", str(cm.exception)) + + def test_missing_name_in_model(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": {"template": [{"content": "some content", "role": "user"}]}, + "model": {}, + }, + } + }] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "Missing configuration for.*model.*name") + + def test_template_ref_in_prompt_templating(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template_ref": "b2eb90fa-bf26-4d5e-87c4-d37f49zshuhf3c" + }, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "template_ref.*not yet supported") + + def test_empty_template_list(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": {"template": []}, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertIn("template list cannot be empty", str(cm.exception)) + + def test_missing_content_in_template_list(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": {"template": [{}]}, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "content.*role") + + def test_image_url_in_content_type(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { + "url": "https://i.natgeofe.com/n/548467d8-c5f1-4551-9f58-6817a8d2c45e/NationalGeographic_2572187_3x2.jpg" + }, + } + ], + } + ] + }, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + with self.assertRaises(RuntimeError) as cm: + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() + self.assertIn("image_url is not supported", str(cm.exception)) + + # write one positive test case where everything is right and no exception is raised + def test_positive_case(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + "role": "user", + } + ] + }, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + validate_orchestration_configuration(run_data, collector) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + +class TestValidateInputParameters(unittest.TestCase): + def test_empty_metrics(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "Sample text", + } + ] + }, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + metrics = [] + metric_templates = [] + with self.assertRaises(RuntimeError) as cm: + validate_input_config(run_data, metrics, metric_templates, collector) + collector.raise_if_errors() + self.assertIn("Metrics list cannot be empty. Atleast one metric needs to be provided", str(cm.exception)) + + def test_valid_input_parameters(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "Sample text", + } + ] + }, + "model": { + "name": "gpt-4", + "version": "latest", + }, + }, + } + }] + metrics = ["bert_score"] + metric_templates = [ + { + "evaluationMethod": "computed", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "bert_score", + "name": "BERT Score", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + # This should not raise any exception + validate_input_config(run_data, metrics, metric_templates, collector) + collector.raise_if_errors() # Should not raise + +class TestValidateVariableMappingOfMetrics(unittest.TestCase): + def test_valid_all_metrics_reference_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference": "value1", "column1": "value2"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/reference" + } + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + def test_invalid_all_metrics_reference_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference-key": "value1", "column1": "value2"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/invalid_column" + } + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + with self.assertRaises(RuntimeError): + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() + + def test_valid_individual_metrics_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference": "value1", "json_schema": "value2"}] + variable_mapping = { + "metric1/reference": "data/reference", + "metric2/json_schema": "data/json_schema", + } + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": ["reference"], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": ["json_schema"], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + def test_invalid_individual_metrics_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference": "value1", "json_schema": "value2"}] + variable_mapping = { + "metric1/invalid_key": "data/reference", + "metric2/json_schema": "data/invalid_column", + } + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + with self.assertRaises(RuntimeError): + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() + + def test_missing_dataset_column_for_all_metrics(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"column1": "value1"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/reference" + } + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + with self.assertRaises(RuntimeError): + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() + + def test_invalid_custom_metric_mapping(self): + collector = ValidationCollector() + + # Use a system metric name (not in mock_custom_metric_data) + metrics = ["metric1", "system_metric"] + + # mock metric templates + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "system_metric", + "name": "System metric", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + + # Dataset has only these columns + template_vars_data = [{"reference": "value1", "json_schema": "value2"}] + + # This mapping refers to a non-existent dataset field, which should trigger validation error + variable_mapping = { + "system_metric/topic": "data/non_existent_column", + "metric1/reference": "data/reference", + } + + with self.assertRaises(RuntimeError): + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() + + def test_valid_custom_metric_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2", "correctness"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "correctness", + "name": "Correctness", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": ["topic"], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric 2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": ["json_schema"], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + template_vars_data = [{"field": "value1", "json_schema": "value2"}] + variable_mapping = { + "correctness/topic": "data/field", + "metric2/json_schema": "data/json_schema", + } + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + def test_missing_dataset_column_for_individual_metrics(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric 1", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric2", + "name": "Metric 2", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + }, + ] + template_vars_data = [{"column1": "value1"}] + variable_mapping = { + "metric1/reference": "data/reference", + "metric2/json_schema": "data/json_schema", + } + with self.assertRaises(RuntimeError): + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + collector.raise_if_errors() + + +class TestValidateVariableMappingOfPrompts(unittest.TestCase): + def test_valid_variable_mapping(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + "role": "user" + } + ] + } + } + } + }] + template_vars_data = [{"var1": "value1", "var2": "value2"}] + variable_mapping = {"prompt/var1": "data/var1", "prompt/var2": "data/var2"} + + validate_variable_mapping_of_prompts( + run_data, template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + def test_missing_variable_in_mapping_and_dataset(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + "role": "user" + } + ] + } + } + } + }] + template_vars_data = [{"var1": "value1"}] + variable_mapping = {"prompt/var1": "data/var1"} + + with self.assertRaises(RuntimeError) as cm: + validate_variable_mapping_of_prompts( + run_data, template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + self.assertIn("The provided prompt variable :var2 in Orch config does not match", str(cm.exception)) + + def test_system_defined_variable_in_prompt(self): + collector = ValidationCollector() + content = "This is a prompt with {{?prompt}}" + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"content": content, "role": "user"}] + } + } + } + }] + template_vars_data = [{"var1": "value1"}] + variable_mapping = {} + + with self.assertRaises(RuntimeError) as cm: + validate_variable_mapping_of_prompts( + run_data, template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + self.assertIn("System defined variables", str(cm.exception)) + + def test_variable_in_dataset_but_not_in_mapping(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + "role": "user" + } + ] + } + } + } + }] + template_vars_data = [{"var1": "value1", "var2": "value2"}] + variable_mapping = {"prompt/var1": "data/var1"} + + validate_variable_mapping_of_prompts( + run_data, template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() # Should not raise + self.assertTrue(True) + + def test_variable_in_mapping_but_not_in_dataset(self): + collector = ValidationCollector() + run_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "content": "This is a prompt with {{?var1}} and {{?var2}}.", + "role": "user" + } + ] + } + } + } + }] + template_vars_data = [{"var1": "value1"}] + variable_mapping = {"prompt/var1": "data/var1", "prompt/var2": "data/var2"} + + with self.assertRaises(RuntimeError) as cm: + validate_variable_mapping_of_prompts( + run_data, template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + self.assertIn("The provided prompt variable :var2", str(cm.exception)) + + +class TestPopulateTemplateVarsDataIfSingleReferenceProvided(unittest.TestCase): + def test_reference_key_directly_present_in_dataset_already_filled(self): + collector = ValidationCollector() + template_vars_data = [ + {"topic": "banana", "reference": "ref_value"}, + {"topic": "apple", "reference": "ref_value"}, + ] + variable_mapping = {} + + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + + # Needs to replicate the reference value using existing value + self.assertEqual(template_vars_data[1]["reference"], "ref_value") + + def test_reference_key_directly_present_in_dataset_single_value(self): + collector = ValidationCollector() + template_vars_data = [ + {"topic": "banana", "reference": "ref_value"}, + {"topic": "apple"}, + ] + variable_mapping = {} + + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + + # Needs to replicate the reference value using existing value + self.assertEqual(template_vars_data[1]["reference"], "ref_value") + + def test_reference_key_present_in_first_row(self): + collector = ValidationCollector() + template_vars_data = [ + {"topic": "ml", "ref_key": "ref_value"}, + {"topic": "ai"}, + {"topic": "gen_ai"}, + ] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/ref_key" + } + + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + + self.assertEqual(template_vars_data[1]["ref_key"], "ref_value") + self.assertEqual(template_vars_data[2]["ref_key"], "ref_value") + + def test_reference_key_missing_in_all_rows(self): + collector = ValidationCollector() + template_vars_data = [{"other_key": "value1"}, {"other_key": "value2"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/ref_key" + } + + with self.assertRaises(RuntimeError): + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + + def test_partial_reference_key_in_dataset(self): + collector = ValidationCollector() + template_vars_data = [ + {"topic": "banana", "reference": "ref_value"}, + {"topic": "apple", "reference": ""}, + {"topic": "apple", "reference": ""}, + {"topic": "apple", "reference": "ref_value"}, + {"topic": "apple", "reference": ""}, + ] + variable_mapping = {} + + with self.assertRaises(RuntimeError): + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + + def test_no_reference_key_in_dataset(self): + collector = ValidationCollector() + template_vars_data = [ + {"topic": "banana"}, + {"topic": "apple"}, + ] + variable_mapping = {"all_metrics/reference": "data/reference"} + + with self.assertRaises(RuntimeError): + populate_dataset_data_if_single_reference_provided( + template_vars_data, variable_mapping, collector + ) + collector.raise_if_errors() + + +class TestPopulateTemplateVarsDataIfIndividualMetricReferenceProvided(unittest.TestCase): + def test_reference_key_directly_present_in_dataset_single_value(self): + collector = ValidationCollector() + template_vars_data = [ + {"metric1_reference": "value1", "column1": "value2"}, + {"column1": "value3"}, + ] + variable_mapping = {"bleu/reference": "data/metric1_reference"} + metrics = ["bleu"] + + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + print("template data now is ", template_vars_data) + self.assertTrue(True) + # assert template_vars_data[1]["metric1_reference"] == "value1" + + def test_reference_key_present_in_first_row(self): + collector = ValidationCollector() + template_vars_data = [ + {"metric1_reference": "value1", "column1": "value2"}, + {"column1": "value3"}, + {"column1": "value4"}, + ] + variable_mapping = {"bertscore/reference": "data/metric1_reference"} + metrics = ["bertscore"] + + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + + self.assertEqual(template_vars_data[1]["metric1_reference"], "value1") + self.assertEqual(template_vars_data[2]["metric1_reference"], "value1") + + def test_reference_key_missing_in_all_rows(self): + collector = ValidationCollector() + template_vars_data = [{"column1": "value1"}, {"column1": "value2"}] + variable_mapping = {"bleu/reference": "data/metric1_reference"} + metrics = ["bleu"] + + with self.assertRaises(RuntimeError): + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + collector.raise_if_errors() + + def test_reference_key_partial_rows(self): + collector = ValidationCollector() + template_vars_data = [ + {"metric1_reference": "value1", "column1": "value2"}, + {"column1": "value3"}, + {"metric1_reference": "value4", "column1": "value5"}, + ] + variable_mapping = {"rouge/reference": "data/metric1_reference"} + metrics = ["rouge"] + + with self.assertRaises(RuntimeError): + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + collector.raise_if_errors() + + def test_reference_key_not_in_variable_mapping(self): + collector = ValidationCollector() + template_vars_data = [ + {"metric1_reference": "value1", "column1": "value2"}, + {"column1": "value3"}, + ] + variable_mapping = {} + metrics = ["metric1"] + + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + + self.assertNotIn("metric1_reference", template_vars_data[1]) + + def test_multiple_metrics_with_reference(self): + collector = ValidationCollector() + template_vars_data = [ + {"metric1_reference": "value1", "metric2_reference": "value2"}, + {"column1": "value3"}, + ] + variable_mapping = { + "rouge/reference": "data/metric1_reference", + "exact-match/reference": "data/metric2_reference", + } + metrics = ["rouge", "exact-match"] + + populate_dataset_data_if_individual_metric_reference_provided( + template_vars_data, variable_mapping, metrics, collector + ) + + self.assertEqual(template_vars_data[1]["metric1_reference"], "value1") + self.assertEqual(template_vars_data[1]["metric2_reference"], "value2") + + +class TestExtractDatasetColumns(unittest.TestCase): + def test_extract_from_list_of_dicts(self): + template_variables = [ + {"key1": "value1", "key2": "value2"}, + {"key1": "value3", "key2": "value4"}, + ] + result = extract_dataset_columns(template_variables) + self.assertEqual(result, ["key1", "key2"]) + + def test_extract_from_empty_list(self): + template_variables = [] + result = extract_dataset_columns(template_variables) + self.assertEqual(result, []) + + def test_extract_from_dict(self): + template_variables = {"key1": "value1", "key2": "value2"} + result = extract_dataset_columns(template_variables) + self.assertEqual(result, ["key1", "key2"]) + + def test_extract_from_empty_dict(self): + template_variables = {} + result = extract_dataset_columns(template_variables) + self.assertEqual(result, []) + + def test_extract_from_non_dict_or_list(self): + template_variables = "invalid_type" + result = extract_dataset_columns(template_variables) + self.assertEqual(result, []) + + + def test_get_user_prompts_from_template_list(self): + """Test with valid prompt templates""" + template_list = [ + {"role": "user", "content": "this is a prompt from user."}, + {"role": "system", "content": "this is a prompt from system."}, + ] + + user_prompt_count = count_user_prompts_from_template_list(template_list) + self.assertEqual(user_prompt_count, 1) + + template_list = [ + {"role": "user", "content": "this is a prompt from user."}, + {"role": "user", "content": "this is another prompt from user."}, + ] + + user_prompt_count = count_user_prompts_from_template_list(template_list) + self.assertEqual(user_prompt_count, 2) + + +class TestCreateCustomMetricName(unittest.TestCase): + def setUp(self): + self.mock_custom_metric = {"metricName": "custom_metric_1", "scenario": "scenario_1", "version": "v1"} + self.mock_custom_metric_missing_version = {"metricName": "custom_metric_1", "scenario": "scenario_1"} + self.mock_custom_metric_metricId = {"metricId": "custom_metric_id"} + self.mock_custom_metric_invalid_metricId = { + "metricId": "custom_metric_id", + "metricName": "custom_metric", + "scenario": "genai-evaluations", + } + self.mock_custom_metric_missing_metric_name = {"scenario": "genai-evaluations", "version": "0.0.1"} + self.mock_custom_metric_missing_scenario = {"metricName": "custom_metric", "version": "0.0.1"} + self.mock_custom_metric_missing_scenario_and_metric_name = {"version": "0.0.1"} + + def test_valid_only_custom_metric_id(self): + """Test with a valid custom metric name.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric_metricId, collector) + self.assertEqual(result, "custom_metric_id") + collector.raise_if_errors() # Should not raise + + def test_invalid_custom_metric_id(self): + """Test with a valid custom metric name.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric_invalid_metricId, collector) + # Function returns the metric_id but adds error to collector + self.assertEqual(result, "custom_metric_id") # Still returns the metric_id + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Both 'metricId' and 'scenario/metricName' cannot be provided at the same time", str(cm.exception)) + + def test_valid_full_custom_metric_name(self): + """Test with a valid custom metric name.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + self.assertEqual(result, "scenario_1/custom_metric_1/v1") + collector.raise_if_errors() # Should not raise + + def test_valid_no_version_custom_metric_name(self): + """Test with a valid custom metric name.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric_missing_version, collector) + self.assertEqual(result, "scenario_1/custom_metric_1") + collector.raise_if_errors() # Should not raise + + def test_invalid_custom_metric_name_without_scenario(self): + """Test with a valid custom metric name but no scenario.""" + self.mock_custom_metric["scenario"] = None + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + # Function still returns a value but adds error + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_empty_metric_name(self): + """Test with an empty metricName.""" + self.mock_custom_metric["metricName"] = "" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'metricName' field in custom metric configuration", str(cm.exception)) + + def test_empty_scenario(self): + """Test with an empty scenario.""" + self.mock_custom_metric["scenario"] = "" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_none_metric_name(self): + """Test with a None metricName.""" + self.mock_custom_metric["metricName"] = None + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'metricName' field in custom metric configuration", str(cm.exception)) + + def test_none_scenario(self): + """Test with a None scenario.""" + self.mock_custom_metric["scenario"] = None + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_both_metric_id_and_scenario_empty(self): + """Test with both metric ID and scenario empty.""" + self.mock_custom_metric["metricName"] = "" + self.mock_custom_metric["scenario"] = "" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_both_metric_id_and_scenario_none(self): + """Test with both metric ID, metricName, and scenario None.""" + self.mock_custom_metric["metricName"] = None + self.mock_custom_metric["scenario"] = None + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_no_required_variables_present_scenario(self): + """Test with a None scenario.""" + collector = ValidationCollector() + result = create_custom_metric_name({}, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_missing_scenario_and_metric_name(self): + """Test with both scenario and metricName missing.""" + collector = ValidationCollector() + result = create_custom_metric_name( + self.mock_custom_metric_missing_scenario_and_metric_name, collector + ) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + def test_missing_metric_name(self): + """Test with a missing metricName.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric_missing_metric_name, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'metricName' field in custom metric configuration", str(cm.exception)) + + def test_missing_scenario(self): + """Test with a missing scenario.""" + collector = ValidationCollector() + result = create_custom_metric_name(self.mock_custom_metric_missing_scenario, collector) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Missing 'scenario' field in custom metric configuration", str(cm.exception)) + + +class TestSelectModelDetailsRandomly(unittest.TestCase): + def test_valid_selection(self): + run1 = { + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "4.0"}, + } + } + } + run2 = { + "modules": { + "prompt_templating": { + "model": {"name": "llama2", "version": "7b"}, + } + } + } + run3 = { + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "4.1"}, + } + } + } + + run_data = [run1, run2, run3] + collector = ValidationCollector() + + name, model_version = select_model_details_randomly(run_data, collector) + collector.raise_if_errors() + + self.assertIn(name, {"gpt-4", "llama2"}) + self.assertIn(model_version, {"4.0", "4.1", "7b"}) + + def test_no_model_data_raises_validation_error(self): + run_data = [] + collector = ValidationCollector() + + result = select_model_details_randomly(run_data, collector) + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + self.assertIsNone(result) + + def test_empty_names(self): + run1 = { + "modules": { + "prompt_templating": { + "model": {"name": "", "version": "4.0"}, + } + } + } + run2 = { + "modules": { + "prompt_templating": { + "model": {"name": "", "version": "4.1"}, + } + } + } + + run_data = [run1, run2] + collector = ValidationCollector() + + result = select_model_details_randomly(run_data, collector) + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + self.assertIsNone(result) + + +class TestCreateModelVersionsMapFromOrchConfigs(unittest.TestCase): + def test_returns_expected_map(self): + orch_config = { + "modules": { + "prompt_templating": {"model": {"name": "gpt-4", "version": "4.0"}} + } + } + collector = ValidationCollector() + result = create_model_versions_map_from_orch_configs([orch_config], collector) + + self.assertEqual(result, {"gpt-4": ["4.0"]}) + collector.raise_if_errors() + + def test_empty_model_data_triggers_collector(self): + orch_config = { + "modules": {"prompt_templating": {"model": {"name": "", "version": ""}}} + } + collector = ValidationCollector() + result = create_model_versions_map_from_orch_configs([orch_config], collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + self.assertIsNone(result) + + +class TestCreateModelVersionsMapFromConfigurationParamBindings(unittest.TestCase): + def test_valid_json_stringified_value(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = json.dumps( + [ + {"modelName": "gpt-4", "modelVersions": ["4.0", "4.1"]}, + {"modelName": "llama2", "modelVersions": ["7b"]}, + ] + ) + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "allow" + param_bindings = [param1, param2] + + collector = ValidationCollector() + result_map, filter_type = create_model_versions_map_from_configuration_param_bindings( + param_bindings, collector + ) + self.assertEqual(result_map, {"gpt-4": ["4.0", "4.1"], "llama2": ["7b"]}) + self.assertEqual(filter_type, "allow") + + def test_model_filter_list_value_is_none(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = None + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "deny" + param_bindings = [param1, param2] + + collector = ValidationCollector() + result_map, filter_type = create_model_versions_map_from_configuration_param_bindings( + param_bindings, collector + ) + self.assertEqual(result_map, {}) + self.assertEqual(filter_type, "deny") + + def test_invalid_json_adds_error(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = "[invalid-json]" + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "allow" + param_bindings = [param1, param2] + + collector = ValidationCollector() + result_map, filter_type = create_model_versions_map_from_configuration_param_bindings( + param_bindings, collector + ) + # Function adds error to collector instead of raising + self.assertEqual(result_map, {}) + self.assertEqual(filter_type, "allow") + + def test_missing_model_filter_list_type(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = json.dumps( + [{"modelName": "gpt-4", "modelVersions": ["latest"]}] + ) + param_bindings = [param1] + + collector = ValidationCollector() + result_map, filter_type = create_model_versions_map_from_configuration_param_bindings( + param_bindings, collector + ) + self.assertEqual(result_map, {"gpt-4": ["latest"]}) + self.assertIsNone(filter_type) + + +class TestParseModelFilterList(unittest.TestCase): + def test_parse_valid_dict_list(self): + param = MagicMock() + param.value = [{"modelName": "gpt-4", "modelVersions": ["4.0"]}] + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + self.assertEqual(result, [{"modelName": "gpt-4", "modelVersions": ["4.0"]}]) + + def test_parse_valid_json_string(self): + param = MagicMock() + param.value = json.dumps([{"modelName": "gpt-4", "modelVersions": ["4.0"]}]) + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + self.assertEqual(result, [{"modelName": "gpt-4", "modelVersions": ["4.0"]}]) + + def test_parse_none_returns_empty(self): + param = MagicMock() + param.value = None + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + self.assertEqual(result, []) + + def test_parse_invalid_json_adds_error(self): + param = MagicMock() + param.value = "[invalid-json" + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + # Function adds error to collector instead of raising + self.assertEqual(result, []) + self.assertTrue(collector.has_errors()) + + def test_parse_type_error_adds_error(self): + param = MagicMock() + param.value = 123 + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + # Function adds error to collector instead of raising + self.assertEqual(result, []) + self.assertTrue(collector.has_errors()) + + def test_parse_string_null_returns_empty(self): + param = MagicMock() + param.value = "null" + collector = ValidationCollector() + result = parse_model_filter_list(param, collector) + self.assertEqual(result, []) + + +class TestBuildModelVersionsMap(unittest.TestCase): + def test_valid_model_list(self): + model_list = [ + {"modelName": "gpt-4", "modelVersions": ["4.0", "4.1"]}, + {"modelName": "llama2", "modelVersions": ["7b"]}, + ] + expected = {"gpt-4": ["4.0", "4.1"], "llama2": ["7b"]} + result = build_model_versions_map(model_list) + self.assertEqual(result, expected) + + def test_model_list_with_missing_name(self): + model_list = [ + {"modelName": "gpt-4", "modelVersions": ["4.0"]}, + {"modelVersions": ["7b"]}, # Missing modelName + ] + expected = {"gpt-4": ["4.0"]} + result = build_model_versions_map(model_list) + self.assertEqual(result, expected) + + def test_empty_model_list(self): + model_list = [] + result = build_model_versions_map(model_list) + self.assertEqual(result, {}) + + +class TestExtractDeploymentId(unittest.TestCase): + def test_url_with_trailing_slash(self): + url = "https://host.domain/api/v1/deployments/deployment123/" + self.assertEqual(extract_deployment_id(url), "deployment123") + + def test_url_without_trailing_slash(self): + url = "https://host.domain/api/v1/deployments/deployment456" + self.assertEqual(extract_deployment_id(url), "deployment456") + +class TestHandleMissingDependentVariables(unittest.TestCase): + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_dataset_columns") + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_metrics_variables") + def test_variable_present_in_dataset(self, mock_extract_vars, mock_extract_cols): + mock_extract_cols.return_value = {"question", "answer"} + mock_extract_vars.return_value = ["question"] + + collector = ValidationCollector() + metrics = ["metric1"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric1", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + dataset = [{"question": "What?", "answer": "Yes"}] + variable_mapping = {} + + handle_missing_dependent_variables_in_dataset( + dataset, metrics, metric_templates, variable_mapping, collector + ) + + self.assertFalse(collector.has_errors()) + + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_dataset_columns") + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_metrics_variables") + def test_variable_mapped_correctly(self, mock_extract_vars, mock_extract_cols): + mock_extract_cols.return_value = {"other_column"} + mock_extract_vars.return_value = ["question"] + + collector = ValidationCollector() + metrics = ["metric1"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric1", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + dataset = [{"other_column": "value"}] + variable_mapping = {"metric1/question": "other_column"} + + handle_missing_dependent_variables_in_dataset( + dataset, metrics, metric_templates, variable_mapping, collector + ) + self.assertFalse(collector.has_errors()) + + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_dataset_columns") + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_metrics_variables") + def test_variable_present_under_all_metrics_mapping( + self, mock_extract_vars, mock_extract_cols + ): + mock_extract_cols.return_value = {"some_column"} + mock_extract_vars.return_value = ["question"] + + collector = ValidationCollector() + metrics = ["metric1"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric1", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + dataset = [{"some_column": "value"}] + variable_mapping = {"all_metrics/question": "some_column"} + + handle_missing_dependent_variables_in_dataset( + dataset, metrics, metric_templates, variable_mapping, collector + ) + self.assertFalse(collector.has_errors()) + + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_dataset_columns") + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_metrics_variables") + def test_missing_variable_and_no_mapping_raises_error( + self, mock_extract_vars, mock_extract_cols + ): + mock_extract_cols.return_value = {"only_column"} + mock_extract_vars.return_value = ["question"] + + collector = ValidationCollector() + metrics = ["metric1"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "metric1", + "name": "Metric1", + "description": "This is a description for Pointwise Correctness.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + dataset = [{"only_column": "data"}] + variable_mapping = {} # no mapping for "question" + + handle_missing_dependent_variables_in_dataset( + dataset, metrics, metric_templates, variable_mapping, collector + ) + + with self.assertRaises(RuntimeError) as e: + collector.raise_if_errors() + + self.assertIn("Invalid mapping", str(e.exception)) + + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_dataset_columns") + @patch("gen_ai_hub.evaluations.utils.gen_utils.extract_metrics_variables") + def test_metric_with_no_dependent_variables_is_skipped( + self, mock_extract_vars, mock_extract_cols + ): + mock_extract_cols.return_value = {"irrelevant"} + mock_extract_vars.return_value = [] + + collector = ValidationCollector() + metrics = ["custom_metric"] + metric_templates = [ + { + "evaluationMethod": "llm-as-a-judge", + "scenario": "genai-evaluations", + "createdAt": "0001-01-01 00:00:00+00:00", + "managedBy": "imperative", + "metricType": "evaluation", + "systemPredefined": True, + "id": "custom_metric", + "name": "Custom Metric", + "description": "This is a description for Bert Score.", + "version": "1.0.0", + "includeProperties": ["reference"], + "additionalProperties": { + "variables": [], + "output_type": "numerical", + "supported_values": [1, 5], + "experimental": False, + }, + } + ] + + dataset = [{"irrelevant": "data"}] + variable_mapping = {} + + handle_missing_dependent_variables_in_dataset( + dataset, metrics, metric_templates, variable_mapping, collector + ) + + self.assertFalse(collector.has_errors()) + + +class TestHandleReferenceMissingRows(unittest.TestCase): + @patch( + "gen_ai_hub.evaluations.utils.gen_utils.populate_dataset_data_if_single_reference_provided" + ) + @patch( + "gen_ai_hub.evaluations.utils.gen_utils.populate_dataset_data_if_individual_metric_reference_provided" + ) + def test_calls_both_population_functions( + self, + mock_populate_individual, + mock_populate_all, + ): + # Arrange + template_vars_data = [{"input": "Hello", "reference": "Hi"}] + variable_mapping = {"allMetrics/reference": "reference"} + metrics = ["metric1", "metric2"] + collector = MagicMock() + + # Act + handle_reference_missing_rows( + template_vars_data, variable_mapping, metrics, collector + ) + + # Assert + mock_populate_all.assert_called_once_with( + template_vars_data, variable_mapping, collector + ) + mock_populate_individual.assert_called_once_with( + template_vars_data, variable_mapping, metrics, collector + ) + + +class TestTemplateContent(unittest.TestCase): + def test_template_content_with_single_list(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "This is a prompt with {{?var1}} and {{?var2}}.", + } + ], + } + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2"}) + + def test_template_content_with_multiple_lists(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "This is a prompt with {{?var1}} and {{?var2}}.", + }, + { + "type": "text", + "text": "This is a prompt with {{?var3}} and {{?var4}}.", + }, + { + "type": "text", + "text": "This is a prompt with {{?var5}} and {{?var6}}.", + }, + ], + } + ] + } + } + } + } + result = get_prompt_variables_from_orch_config(orch_config) + self.assertEqual(result, {"var1", "var2", "var3", "var4", "var5", "var6"}) + + +# Tests for uncovered lines +class TestUpdateVariableMapping(unittest.TestCase): + def test_update_variable_mapping(self): + """Test update_variable_mapping function""" + variable_mapping = {"var1": "field1", "var2": "field2"} + prefix_key = "prompt/" + variable_mapping_dict = {} + + result = update_variable_mapping(variable_mapping, prefix_key, variable_mapping_dict) + + self.assertEqual(result, { + "prompt/var1": "data/field1", + "prompt/var2": "data/field2" + }) + self.assertEqual(variable_mapping_dict, result) # Should modify the dict in place + + def test_update_variable_mapping_with_existing_dict(self): + """Test update_variable_mapping with existing dict""" + variable_mapping = {"var1": "field1"} + prefix_key = "metric1/" + variable_mapping_dict = {"existing": "value"} + + result = update_variable_mapping(variable_mapping, prefix_key, variable_mapping_dict) + + self.assertIn("metric1/var1", result) + self.assertIn("existing", result) + + +class TestGetAccumulatedConfigData(unittest.TestCase): + def test_get_accumulated_config_data_single(self): + """Test get_accumulated_config_data with single config""" + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping={"prompt/input": "data/input"}, + ) + + result = get_accumulated_config_data([config1]) + + self.assertEqual(result.orch_config_data, config1.orch_config_data) + self.assertEqual(result.metrics_list, ["metric1"]) + self.assertEqual(result.variable_mapping, {"prompt/input": "data/input"}) + + def test_get_accumulated_config_data_multiple(self): + """Test get_accumulated_config_data with multiple configs""" + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test1"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping={"prompt/input": "data/input"}, + ) + config2 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "llama2"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test1"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping={"prompt/output": "data/output"}, + ) + + result = get_accumulated_config_data([config1, config2]) + + self.assertEqual(len(result.orch_config_data), 2) + self.assertEqual(result.variable_mapping, {"prompt/input": "data/input", "prompt/output": "data/output"}) + + def test_get_accumulated_config_data_with_none_mapping(self): + """Test get_accumulated_config_data when variable_mapping is None""" + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping=None, + ) + + result = get_accumulated_config_data([config1]) + + self.assertEqual(result.variable_mapping, {}) + + def test_get_accumulated_config_data_exception_handling(self): + """Test get_accumulated_config_data exception handling""" + # Create a config that will cause an exception inside the try block + # The exception should be caught and wrapped in RuntimeError + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping={"prompt/input": "data/input"}, + ) + + # Create a bad config that will cause an error when extending + class BadOrchConfig: + def __iter__(self): + raise ValueError("Error during iteration") + + bad_config = _EvaluationConfigData( + orch_config_data=BadOrchConfig(), # This will cause an error when extending + dataset_type="json", + dataset_data={"row1": {"input": "test"}}, + metric_templates=[{"id": "metric1"}], + metrics_list=["metric1"], + variable_mapping=None, + ) + + with self.assertRaises(RuntimeError) as cm: + get_accumulated_config_data([config1, bad_config]) + self.assertRegex(str(cm.exception), "Failed to accumulate") + + +class TestCreateModelVersionsMapFromCustomMetricConfig(unittest.TestCase): + def test_create_model_versions_map_from_custom_metric_config(self): + """Test create_model_versions_map_from_custom_metric_config""" + custom_metric_config_data = [ + { + "metricId": "metric1", + MODEL_CONFIGURATION_KEY: { + MODEL_NAME_KEY: "gpt-4", + MODEL_VERSION_KEY: "4.0", + } + }, + { + "metricId": "metric2", + MODEL_CONFIGURATION_KEY: { + MODEL_NAME_KEY: "llama2", + MODEL_VERSION_KEY: "7b", + } + }, + ] + + result = create_model_versions_map_from_custom_metric_config(custom_metric_config_data) + + self.assertEqual(result, {"gpt-4": ["4.0"], "llama2": ["7b"]}) + + def test_create_model_versions_map_from_custom_metric_config_empty(self): + """Test create_model_versions_map_from_custom_metric_config with empty list""" + result = create_model_versions_map_from_custom_metric_config([]) + self.assertEqual(result, {}) + + def test_create_model_versions_map_from_custom_metric_config_none(self): + """Test create_model_versions_map_from_custom_metric_config with None""" + result = create_model_versions_map_from_custom_metric_config(None) + self.assertEqual(result, {}) + + def test_create_model_versions_map_from_custom_metric_config_with_latest(self): + """Test create_model_versions_map_from_custom_metric_config with latest version""" + custom_metric_config_data = [ + { + "metricId": "metric1", + MODEL_CONFIGURATION_KEY: { + MODEL_NAME_KEY: "gpt-4", + # No version specified, should use LATEST_MODEL_VERSION_KEY + } + }, + ] + + result = create_model_versions_map_from_custom_metric_config(custom_metric_config_data) + + self.assertEqual(result, {"gpt-4": [LATEST_MODEL_VERSION_KEY]}) + + def test_create_model_versions_map_from_custom_metric_config_missing_model_name(self): + """Test create_model_versions_map_from_custom_metric_config with missing model name""" + custom_metric_config_data = [ + { + "metricId": "metric1", + MODEL_CONFIGURATION_KEY: { + MODEL_VERSION_KEY: "4.0", + # Missing model name + } + }, + ] + + result = create_model_versions_map_from_custom_metric_config(custom_metric_config_data) + + self.assertEqual(result, {}) # Should not add entry if model_name is missing + + +class TestUpdateTestOrchConfigException(unittest.TestCase): + def test_update_test_orch_config_exception_handling(self): + """Test update_test_orch_config exception handling""" + collector = ValidationCollector() + # Mock ORCHESTRATION_CONFIGURATION_V2 to raise an exception + with patch("gen_ai_hub.evaluations.utils.gen_utils.ORCHESTRATION_CONFIGURATION_V2", {}): + result = update_test_orch_config("gpt-4", "4.0", collector) + + self.assertIsNone(result) + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Error updating model name and version", str(cm.exception)) + + +class TestValidateMetricName(unittest.TestCase): + def test_validate_metric_name_empty(self): + """Test validate_metric_name with empty metric""" + collector = ValidationCollector() + all_supported_metrics = ["bert_score", "bleu"] + + validate_metric_name("", all_supported_metrics, collector) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Metric name cannot be empty", str(cm.exception)) + + def test_validate_metric_name_unsupported(self): + """Test validate_metric_name with unsupported metric""" + collector = ValidationCollector() + all_supported_metrics = ["bert_score", "bleu"] + + validate_metric_name("unknown_metric", all_supported_metrics, collector) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("unknown_metric is neither a system supported metric", str(cm.exception)) + + def test_validate_metric_name_valid(self): + """Test validate_metric_name with valid metric""" + collector = ValidationCollector() + all_supported_metrics = ["bert_score", "bleu"] + + validate_metric_name("bert_score", all_supported_metrics, collector) + + collector.raise_if_errors() # Should not raise + + +class TestCheckIfMetricIsDefinedWithSlash(unittest.TestCase): + def test_check_if_metric_is_defined_with_slash_format(self): + """Test check_if_metric_is_defined with metric in scenario/name/version format""" + collector = ValidationCollector() + metrics = ["scenario1/metric1/v1.0"] + metric_templates = [ + { + "id": "metric1", + "scenario": "scenario1", + "name": "metric1", + "version": "v1.0", + } + ] + + check_if_metric_is_defined(metrics, metric_templates, collector) + + collector.raise_if_errors() # Should not raise + + def test_check_if_metric_is_defined_with_slash_format_not_found(self): + """Test check_if_metric_is_defined with metric in slash format not found""" + collector = ValidationCollector() + metrics = ["scenario1/metric1/v1.0"] + metric_templates = [ + { + "id": "metric1", + "scenario": "scenario2", # Different scenario + "name": "metric1", + "version": "v1.0", + } + ] + + check_if_metric_is_defined(metrics, metric_templates, collector) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("scenario1/metric1/v1.0 is neither a system supported metric", str(cm.exception)) + + +class TestExtractMetricsVariablesWithMetricName(unittest.TestCase): + def test_extract_metrics_variables_with_metric_name(self): + """Test extract_metrics_variables with metric_name parameter""" + metric_templates = [ + { + "id": "metric1", + "name": "Metric 1", + "additionalProperties": { + "variables": ["var1", "var2"] + } + }, + { + "id": "metric2", + "name": "Metric 2", + "additionalProperties": { + "variables": ["var3"] + } + }, + ] + + result = extract_metrics_variables(metric_templates, "metric1") + + self.assertEqual(result, ["var1", "var2"]) + + def test_extract_metrics_variables_with_metric_name_by_name_key(self): + """Test extract_metrics_variables with metric_name matching name key""" + metric_templates = [ + { + "id": "metric1", + "name": "Metric 1", + "additionalProperties": { + "variables": ["var1", "var2"] + } + }, + ] + + result = extract_metrics_variables(metric_templates, "Metric 1") + + self.assertEqual(result, ["var1", "var2"]) + + +class TestValidateIndividualCustomMetrics(unittest.TestCase): + def test_validate_individual_custom_metrics(self): + """Test validate_individual_custom_metrics""" + collector = ValidationCollector() + variable_mapping = { + "custom_metric1/var1": "data/field1", + } + dataset_columns = ["field1"] + custom_metric_ids = ["custom_metric1"] + custom_metric_variables = {"var1"} + + validate_individual_custom_metrics( + variable_mapping, + dataset_columns, + custom_metric_ids, + custom_metric_variables, + collector, + ) + + collector.raise_if_errors() # Should not raise + + def test_validate_individual_custom_metrics_removes_system_variables(self): + """Test validate_individual_custom_metrics removes predefined system variables""" + collector = ValidationCollector() + variable_mapping = {} + dataset_columns = ["var1"] # var1 is in dataset, so no error after system var is removed + custom_metric_ids = [] + custom_metric_variables = {AICORE_LLM_PROMPT_TEMPLATE_KEY, "var1"} + + validate_individual_custom_metrics( + variable_mapping, + dataset_columns, + custom_metric_ids, + custom_metric_variables, + collector, + ) + + # Should not raise since system variable is removed and var1 is in dataset + collector.raise_if_errors() + + +class TestValidateEmptyMappingWithCustomVars(unittest.TestCase): + def test_validate_empty_mapping_with_custom_vars(self): + """Test _validate_empty_mapping_with_custom_vars""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_empty_mapping_with_custom_vars + + collector = ValidationCollector() + variable_mapping = {} + dataset_columns = ["field1"] + custom_metric_variables = {"var1", "var2"} + + _validate_empty_mapping_with_custom_vars( + variable_mapping, + dataset_columns, + custom_metric_variables, + collector, + ) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Variable mapping is empty", str(cm.exception)) + + def test_validate_empty_mapping_with_custom_vars_all_in_dataset(self): + """Test _validate_empty_mapping_with_custom_vars when all vars in dataset""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_empty_mapping_with_custom_vars + + collector = ValidationCollector() + variable_mapping = {} + dataset_columns = ["var1", "var2"] + custom_metric_variables = {"var1", "var2"} + + _validate_empty_mapping_with_custom_vars( + variable_mapping, + dataset_columns, + custom_metric_variables, + collector, + ) + + collector.raise_if_errors() # Should not raise + + +class TestValidateMappingEntry(unittest.TestCase): + def test_validate_mapping_entry_short_key(self): + """Test _validate_mapping_entry with key less than 3 parts""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_mapping_entry + + collector = ValidationCollector() + key = "metric/var" # Only 2 parts + value = "data/field" + custom_metric_ids = ["metric"] + custom_metric_variables = {"var"} + dataset_columns = ["field"] + + _validate_mapping_entry( + key, value, custom_metric_ids, custom_metric_variables, dataset_columns, collector + ) + + collector.raise_if_errors() # Should not raise (early return) + + def test_validate_mapping_entry_not_custom_metric(self): + """Test _validate_mapping_entry with non-custom metric key""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_mapping_entry + + collector = ValidationCollector() + key = "all_metrics/var1" # COLUMN_MAPPING_DEFAULT_KEYS + value = "data/field" + custom_metric_ids = [] + custom_metric_variables = set() + dataset_columns = ["field"] + + _validate_mapping_entry( + key, value, custom_metric_ids, custom_metric_variables, dataset_columns, collector + ) + + collector.raise_if_errors() # Should not raise (early return) + + def test_validate_mapping_entry_invalid_variable(self): + """Test _validate_mapping_entry with invalid variable""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_mapping_entry + + collector = ValidationCollector() + # Key needs at least 3 parts for the function to process it + key = "scenario1/metric1/invalid_var" + value = "data/field" + custom_metric_ids = ["scenario1/metric1"] + custom_metric_variables = {"valid_var"} + # invalid_var is NOT in custom_metric_variables AND NOT in dataset_columns + dataset_columns = ["field"] + + _validate_mapping_entry( + key, value, custom_metric_ids, custom_metric_variables, dataset_columns, collector + ) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("Invalid mapping value provided", str(cm.exception)) + + def test_validate_mapping_entry_missing_dataset_column(self): + """Test _validate_mapping_entry with missing dataset column""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_mapping_entry + + collector = ValidationCollector() + # Key needs at least 3 parts for the function to process it + key = "scenario1/metric1/var1" + value = "data/missing_field" + custom_metric_ids = ["scenario1/metric1"] + custom_metric_variables = {"var1"} + # var1 is in custom_metric_variables, so first check passes + # missing_field (dataset_value) is not in dataset_columns, so second check should fail + dataset_columns = ["other_field"] + + _validate_mapping_entry( + key, value, custom_metric_ids, custom_metric_variables, dataset_columns, collector + ) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertRegex(str(cm.exception), "The provided mapping.*is not valid") + + def test_validate_mapping_entry_value_without_slash(self): + """Test _validate_mapping_entry with value without slash""" + from gen_ai_hub.evaluations.utils.gen_utils import _validate_mapping_entry + + collector = ValidationCollector() + key = "custom_metric1/var1" + value = "just_field" # No "data/" prefix + custom_metric_ids = ["custom_metric1"] + custom_metric_variables = {"var1"} + dataset_columns = ["just_field"] + + _validate_mapping_entry( + key, value, custom_metric_ids, custom_metric_variables, dataset_columns, collector + ) + + collector.raise_if_errors() # Should not raise + + +class TestHandleJsonSchemaMatch(unittest.TestCase): + def test_handle_json_schema_match(self): + """Test handle_json_schema_match""" + collector = ValidationCollector() + metrics = [JSON_SCHEMA_MATCH_METRIC_ID, "other_metric"] + dataset_data = [ + {"json_schema": '{"type": "object"}'}, + {"other": "data"}, + ] + variable_mapping = {} + + handle_json_schema_match(metrics, dataset_data, variable_mapping, collector) + + # Should populate missing rows + self.assertIn("json_schema", dataset_data[1]) + collector.raise_if_errors() # Should not raise + + def test_handle_json_schema_match_not_in_metrics(self): + """Test handle_json_schema_match when metric not in list""" + collector = ValidationCollector() + metrics = ["other_metric"] + dataset_data = [{"other": "data"}] + variable_mapping = {} + + handle_json_schema_match(metrics, dataset_data, variable_mapping, collector) + + # Should not modify data + self.assertNotIn("json_schema", dataset_data[0]) + + +class TestValidateLanguageCodeAndDataPopulation(unittest.TestCase): + def test_validate_language_code_and_data_population_valid(self): + """Test validate_language_code_and_data_population with valid language codes""" + collector = ValidationCollector() + dataset_data = [ + {LANGUAGE_KEY: "en"}, + {LANGUAGE_KEY: "fr"}, + ] + variable_mapping = {} + + validate_language_code_and_data_population(dataset_data, variable_mapping, collector) + + collector.raise_if_errors() # Should not raise + + def test_validate_language_code_and_data_population_missing_value(self): + """Test validate_language_code_and_data_population with missing values""" + collector = ValidationCollector() + dataset_data = [ + {LANGUAGE_KEY: "en"}, + {}, # Missing key entirely + ] + variable_mapping = {} + + validate_language_code_and_data_population(dataset_data, variable_mapping, collector) + + # After populate_dataset_data_if_data_missing, the empty row should have "en" + # But if it's still None or empty after population, it should error + # Actually, the function populates first, so we need a case where all are empty + dataset_data2 = [ + {}, # Missing key + {}, # Missing key + ] + collector2 = ValidationCollector() + validate_language_code_and_data_population(dataset_data2, variable_mapping, collector2) + + # Should error because no valid value to populate from + with self.assertRaises(RuntimeError): + collector2.raise_if_errors() + + def test_validate_language_code_and_data_population_invalid_code(self): + """Test validate_language_code_and_data_population with invalid language code""" + collector = ValidationCollector() + dataset_data = [ + {LANGUAGE_KEY: "invalid_lang_code"}, + ] + variable_mapping = {} + + validate_language_code_and_data_population(dataset_data, variable_mapping, collector) + + with self.assertRaises(RuntimeError) as cm: + collector.raise_if_errors() + self.assertIn("is not supported by the language match metric", str(cm.exception)) + + def test_validate_language_code_and_data_population_with_mapping(self): + """Test validate_language_code_and_data_population with variable mapping""" + collector = ValidationCollector() + dataset_data = [ + {"lang_field": "en"}, + {"lang_field": "fr"}, + ] + variable_mapping = {f"{LANGUAGE_MATCH_METRIC_ID}/{LANGUAGE_KEY}": "data/lang_field"} + + validate_language_code_and_data_population(dataset_data, variable_mapping, collector) + + collector.raise_if_errors() # Should not raise + + +class TestHandleLanguageMatch(unittest.TestCase): + def test_handle_language_match(self): + """Test handle_language_match""" + collector = ValidationCollector() + metrics = [LANGUAGE_MATCH_METRIC_ID, "other_metric"] + dataset_data = [ + {LANGUAGE_KEY: "en"}, + ] + variable_mapping = {} + + handle_language_match(metrics, dataset_data, variable_mapping, collector) + + collector.raise_if_errors() # Should not raise + + def test_handle_language_match_not_in_metrics(self): + """Test handle_language_match when metric not in list""" + collector = ValidationCollector() + metrics = ["other_metric"] + dataset_data = [{"other": "data"}] + variable_mapping = {} + + handle_language_match(metrics, dataset_data, variable_mapping, collector) + + # Should not raise or modify + collector.raise_if_errors() # Should not raise + + +class TestUpdateArtifactDict(unittest.TestCase): + def test_update_artifact_dict_with_string(self): + """Test update_artifact_dict with string artifact""" + artifact_reference = ArtifactSource( + artifact="artifact-id-123", + file_type="json", + ) + artifact_dict_count = {} + + update_artifact_dict(artifact_reference, artifact_dict_count) + + self.assertEqual(artifact_dict_count, {"artifact-id-123": 1}) + + # Test incrementing + update_artifact_dict(artifact_reference, artifact_dict_count) + self.assertEqual(artifact_dict_count, {"artifact-id-123": 2}) + + def test_update_artifact_dict_with_artifact_object(self): + """Test update_artifact_dict with Artifact object""" + from ai_api_client_sdk.models.artifact import Artifact + + mock_artifact = MagicMock(spec=Artifact) + mock_artifact.id = "artifact-uuid-456" + + artifact_reference = ArtifactSource( + artifact=mock_artifact, + file_type="json", + ) + artifact_dict_count = {} + + update_artifact_dict(artifact_reference, artifact_dict_count) + + self.assertEqual(artifact_dict_count, {"artifact-uuid-456": 1}) + + +class TestResolveOrchestrationConfigV2(unittest.TestCase): + def test_resolve_orchestration_config_v2(self): + """Test resolve_orchestration_config_v2""" + template1 = PromptTemplate( + role="user", + content="Template 1 with {{?var1}}" + ) + template2 = PromptTemplate( + role="system", + content="Template 2" + ) + llm = LLM( + name="gpt-4", + version="4.0", + params={"temperature": 0.7} + ) + + result = resolve_orchestration_config_v2([template1, template2], llm) + + self.assertIn(MODULES_KEY, result) + self.assertIn(PROMPT_TEMPLATING_KEY, result[MODULES_KEY]) + self.assertIn(PROMPT_KEY, result[MODULES_KEY][PROMPT_TEMPLATING_KEY]) + self.assertIn(TEMPLATE_KEY, result[MODULES_KEY][PROMPT_TEMPLATING_KEY][PROMPT_KEY]) + self.assertEqual(len(result[MODULES_KEY][PROMPT_TEMPLATING_KEY][PROMPT_KEY][TEMPLATE_KEY]), 2) + self.assertIn(MODEL_KEY, result[MODULES_KEY][PROMPT_TEMPLATING_KEY]) + self.assertEqual(result[MODULES_KEY][PROMPT_TEMPLATING_KEY][MODEL_KEY]["name"], "gpt-4") + self.assertEqual(result[MODULES_KEY][PROMPT_TEMPLATING_KEY][MODEL_KEY]["version"], "4.0") + self.assertEqual(result[MODULES_KEY][PROMPT_TEMPLATING_KEY][MODEL_KEY]["parameters"], {"temperature": 0.7}) diff --git a/packages/gen/tests/evaluations/test_metric_client_utils.py b/packages/gen/tests/evaluations/test_metric_client_utils.py new file mode 100644 index 0000000..fe04c33 --- /dev/null +++ b/packages/gen/tests/evaluations/test_metric_client_utils.py @@ -0,0 +1,280 @@ +import unittest +from unittest.mock import MagicMock, patch + +from gen_ai_hub.evaluations.utils.metric_client_utils import ( + _get_custom_metric_details, + get_custom_metric_by_id, + get_metric_template_info_from_server, + get_metric_version_history, + fetch_all_system_predefined_metrics, +) +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector + + +class TestGetCustomMetricDetails(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.base_url = "https://test.com/v2" + self.mock_ai_core_client.rest_client.get_token.return_value = "Bearer token123" + self.resource_group = "test-rg" + self.error_collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_custom_metric_details_success(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {"resources": [{"id": "1", "name": "metric1"}]} + mock_get.return_value = mock_response + + result = _get_custom_metric_details( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertEqual(result, {"resources": [{"id": "1", "name": "metric1"}]}) + mock_get.assert_called_once() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_custom_metric_details_exception(self, mock_get): + mock_get.side_effect = Exception("Network error") + + result = _get_custom_metric_details( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIsNone(result) + self.assertTrue(len(self.error_collector.errors) > 0) + self.assertIn("GenAI metrics server GET request", self.error_collector.errors[0][1]) + + +class TestGetCustomMetricById(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.base_url = "https://test.com/v2" + self.mock_ai_core_client.rest_client.get_token.return_value = "Bearer token123" + self.resource_group = "test-rg" + self.error_collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_custom_metric_by_id_success(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "metric-123", + "name": "test-metric", + "description": "Test metric description", + } + mock_get.return_value = mock_response + + result = get_custom_metric_by_id( + "metric-123", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertEqual(result["id"], "metric-123") + self.assertEqual(result["name"], "test-metric") + mock_get.assert_called_once() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_custom_metric_by_id_exception(self, mock_get): + mock_get.side_effect = Exception("API error") + + result = get_custom_metric_by_id( + "metric-123", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIsNone(result) + self.assertTrue(len(self.error_collector.errors) > 0) + + +class TestGetMetricTemplateInfoFromServer(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.base_url = "https://test.com/v2" + self.mock_ai_core_client.rest_client.get_token.return_value = "Bearer token123" + self.resource_group = "test-rg" + self.error_collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.get_custom_metric_by_id") + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_get_metric_template_info_found(self, mock_get_details, mock_get_by_id): + mock_get_details.return_value = { + "resources": [ + {"id": "metric-1", "name": "test-metric"}, + {"id": "metric-2", "name": "other-metric"}, + ] + } + mock_get_by_id.return_value = { + "id": "metric-1", + "name": "test-metric", + "schema": {}, + } + + result = get_metric_template_info_from_server( + "test-metric", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIsNotNone(result) + self.assertEqual(result["id"], "metric-1") + mock_get_by_id.assert_called_once_with( + "metric-1", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.get_custom_metric_by_id") + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_get_metric_template_info_not_found(self, mock_get_details, mock_get_by_id): + mock_get_details.return_value = { + "resources": [ + {"id": "metric-1", "name": "other-metric"}, + ] + } + + result = get_metric_template_info_from_server( + "nonexistent-metric", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIsNone(result) + mock_get_by_id.assert_not_called() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_get_metric_template_info_empty_resources(self, mock_get_details): + mock_get_details.return_value = {"resources": []} + + result = get_metric_template_info_from_server( + "test-metric", self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIsNone(result) + + +class TestGetMetricVersionHistory(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.base_url = "https://test.com/v2" + self.mock_ai_core_client.rest_client.get_token.return_value = "Bearer token123" + self.resource_group = "test-rg" + self.error_collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_metric_version_history_success(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "resources": [ + {"version": "1.0", "metricId": "metric-1"}, + {"version": "1.1", "metricId": "metric-1"}, + ] + } + mock_get.return_value = mock_response + + result = get_metric_version_history( + "scenario1", + "metric-1", + "1.0", + self.mock_ai_core_client, + self.resource_group, + self.error_collector, + ) + + self.assertIsNotNone(result) + self.assertEqual(result["version"], "1.0") + mock_get.assert_called_once() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_metric_version_history_no_resources(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {"resources": []} + mock_get.return_value = mock_response + + result = get_metric_version_history( + "scenario1", + "metric-1", + "1.0", + self.mock_ai_core_client, + self.resource_group, + self.error_collector, + ) + + self.assertIsNone(result) + self.assertTrue(len(self.error_collector.errors) > 0) + self.assertIn("No version history resources found", self.error_collector.errors[0][1]) + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils.requests.get") + def test_get_metric_version_history_exception(self, mock_get): + mock_get.side_effect = Exception("Request failed") + + result = get_metric_version_history( + "scenario1", + "metric-1", + "1.0", + self.mock_ai_core_client, + self.resource_group, + self.error_collector, + ) + + self.assertIsNone(result) + self.assertTrue(len(self.error_collector.errors) > 0) + self.assertIn("GenAI metrics server GET request encountered an exception", self.error_collector.errors[0][1]) + + +class TestFetchAllSystemPredefinedMetrics(unittest.TestCase): + def setUp(self): + self.mock_ai_core_client = MagicMock() + self.mock_ai_core_client.base_url = "https://test.com/v2" + self.mock_ai_core_client.rest_client.get_token.return_value = "Bearer token123" + self.resource_group = "test-rg" + self.error_collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_fetch_all_system_predefined_metrics_success(self, mock_get_details): + mock_get_details.return_value = { + "resources": [ + {"id": "1", "name": "metric1", "systemPredefined": True}, + {"id": "2", "name": "metric2", "systemPredefined": False}, + {"id": "3", "name": "metric3", "systemPredefined": True}, + ] + } + + result = fetch_all_system_predefined_metrics( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertEqual(len(result), 2) + self.assertTrue(all(item.get("systemPredefined") for item in result)) + self.assertEqual(result[0]["id"], "1") + self.assertEqual(result[1]["id"], "3") + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_fetch_all_system_predefined_metrics_empty(self, mock_get_details): + mock_get_details.return_value = {"resources": []} + + result = fetch_all_system_predefined_metrics( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertEqual(len(result), 0) + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_fetch_all_system_predefined_metrics_no_predefined(self, mock_get_details): + mock_get_details.return_value = { + "resources": [ + {"id": "1", "name": "metric1", "systemPredefined": False}, + {"id": "2", "name": "metric2", "systemPredefined": False}, + ] + } + + result = fetch_all_system_predefined_metrics( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertEqual(len(result), 0) + + @patch("gen_ai_hub.evaluations.utils.metric_client_utils._get_custom_metric_details") + def test_fetch_all_system_predefined_metrics_exception(self, mock_get_details): + mock_get_details.side_effect = Exception("Server error") + + with self.assertRaises(RuntimeError) as context: + fetch_all_system_predefined_metrics( + self.mock_ai_core_client, self.resource_group, self.error_collector + ) + + self.assertIn("System Predefined metrics obtained from Metric Management service failed", str(context.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/tests/evaluations/test_models.py b/packages/gen/tests/evaluations/test_models.py new file mode 100644 index 0000000..a8f99e0 --- /dev/null +++ b/packages/gen/tests/evaluations/test_models.py @@ -0,0 +1,185 @@ +import unittest +from pathlib import Path + +from ai_api_client_sdk.models.artifact import Artifact +from gen_ai_hub.evaluations.models.artifact_source import ArtifactSource +from gen_ai_hub.evaluations.models.dataset_config import Dataset +from gen_ai_hub.evaluations.models.evaluation_config import EvaluationConfig +from gen_ai_hub.evaluations.models.metric_config import MetricConfig, MetricRef +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails as LLM +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID +from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec + + +class TestArtifactSource(unittest.TestCase): + """Unit tests for ArtifactSource model""" + + def test_artifact_source_with_artifact_object(self): + artifact_obj = Artifact( + id="abc123", + name="test-artifact", + url="ai://default/path", + kind="dataset", + scenario_id="scenario-123", + created_at="2025-11-12T08:40:13Z", + modified_at="2025-11-12T08:40:13Z", + ) + + src = ArtifactSource( + file_type="csv", + artifact=artifact_obj, + path="folder/data.csv", + ) + + self.assertIs(src.artifact, artifact_obj) + self.assertEqual(src.path, "folder/data.csv") + self.assertEqual(src.file_type, "csv") + + def test_artifact_source_with_artifact_id_as_string(self): + artifact_id = "xyz789" + + src = ArtifactSource( + file_type="json", + artifact=artifact_id, + path="data/sample.json", + ) + + self.assertEqual(src.artifact, artifact_id) + self.assertEqual(src.path, "data/sample.json") + self.assertEqual(src.file_type, "json") + + def test_artifact_source_without_path(self): + artifact_id = "id-no-path" + + src = ArtifactSource( + file_type="jsonl", + artifact=artifact_id, + path=None, + ) + + self.assertEqual(src.artifact, artifact_id) + self.assertIsNone(src.path) + self.assertEqual(src.file_type, "jsonl") + + def test_artifact_source_invalid_file_type_is_accepted(self): + src = ArtifactSource( + file_type="txt", + artifact="abc", + path="data/file.txt", + ) + + self.assertEqual(src.file_type, "txt") + + +class TestDataset(unittest.TestCase): + + def test_dataset_with_path_string_json(self): + ds = Dataset("data/sample.json") + self.assertEqual(ds.file_type, "json") + + def test_dataset_with_path_string_csv(self): + ds = Dataset("folder/file.csv") + self.assertEqual(ds.file_type, "csv") + + def test_dataset_with_pathlib_path(self): + ds = Dataset(Path("root/data.jsonl")) + self.assertEqual(ds.file_type, "jsonl") + + def test_dataset_with_unsupported_extension(self): + ds = Dataset("data/sample.unknown") + self.assertIsNone(ds.file_type) + + def test_dataset_with_artifact_source_uses_file_type(self): + artifact_src = ArtifactSource( + file_type="csv", + artifact="abc-123", + path="dataset/data.csv", + ) + + ds = Dataset(artifact_src) + + self.assertEqual(ds.file_type, "csv") + self.assertIs(ds.source, artifact_src) + + +class TestEvaluationConfig(unittest.TestCase): + + def setUp(self): + self.dataset = Dataset("data/sample.json") + self.metrics = [MetricConfig(reference=MetricRef(name="bert-score"))] + + def test_initialization_with_llm_and_template_string(self): + llm = LLM(name="gpt-test", version="latest", params={"temperature": 0.7}) + + cfg = EvaluationConfig( + dataset_config=self.dataset, + metrics=self.metrics, + llm=llm, + template="Write a caption about {{?topic}}" + ) + + self.assertIs(cfg.llm, llm) + self.assertEqual(cfg.template, "Write a caption about {{?topic}}") + self.assertIsNone(cfg.orchestration_registry_reference) + + def test_initialization_with_llm_and_template_ref(self): + llm = LLM(name="gpt-test", version="latest", params={"temperature": 0.7}) + t_ref = TemplateRef(template_ref=TemplateRefByID(id="abc123")) + + cfg = EvaluationConfig( + dataset_config=self.dataset, + metrics=self.metrics, + llm=llm, + template=t_ref + ) + + self.assertIs(cfg.llm, llm) + self.assertIs(cfg.template, t_ref) + self.assertIsNone(cfg.orchestration_registry_reference) + + def test_initialization_with_llm_and_template_spec(self): + template_spec = PromptTemplateSpec( + template=[{"role": "user", "content": "Explain {{?concept}}"}], + defaults={} + ) + + cfg = EvaluationConfig( + dataset_config=self.dataset, + metrics=self.metrics, + llm=LLM(name="gpt-test"), + template=template_spec, + template_variable_mapping={"concept": "topic"} + ) + + self.assertIs(cfg.template, template_spec) + self.assertEqual(cfg.template_variable_mapping, {"concept": "topic"}) + + def test_initialization_with_orchestration_registry_reference(self): + cfg = EvaluationConfig( + dataset_config=self.dataset, + metrics=self.metrics, + orchestration_registry_reference="a1b2c3d4-1234-5678-9999-abcdefabcdef" + ) + + self.assertEqual( + cfg.orchestration_registry_reference, + "a1b2c3d4-1234-5678-9999-abcdefabcdef" + ) + self.assertIsNone(cfg.llm) + self.assertIsNone(cfg.template) + + def test_initialization_with_optional_parameters(self): + cfg = EvaluationConfig( + dataset_config=self.dataset, + metrics=self.metrics, + llm=LLM(name="gpt-test"), + test_row_count=50, + repetitions=3, + tags={"team": "ai-core"}, + debug_mode=True + ) + + self.assertEqual(cfg.test_row_count, 50) + self.assertEqual(cfg.repetitions, 3) + self.assertEqual(cfg.tags, {"team": "ai-core"}) + self.assertTrue(cfg.debug_mode) diff --git a/packages/gen/tests/evaluations/test_s3_file_client.py b/packages/gen/tests/evaluations/test_s3_file_client.py new file mode 100755 index 0000000..ec16e8f --- /dev/null +++ b/packages/gen/tests/evaluations/test_s3_file_client.py @@ -0,0 +1,467 @@ +import json +import sqlite3 +import tempfile +import unittest +from unittest.mock import MagicMock, patch +from io import BytesIO + +import gen_ai_hub.evaluations.helpers.s3_file_client as module +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.s3_file_client import S3FileClient + + +class TestS3FileClient(unittest.TestCase): + + def setUp(self): + self.collector = ValidationCollector() + + def _mock_boto_session(self, mock_s3=None): + if mock_s3 is None: + mock_s3 = MagicMock() + + patcher = patch("boto3.Session") + self.addCleanup(patcher.stop) + mock_session = patcher.start() + + session_obj = MagicMock() + session_obj.client.return_value = mock_s3 + session_obj.region_name = "us-east-1" + mock_session.return_value = session_obj + + return mock_s3 + + def test_init_valid_bucket(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.return_value = True + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertIsInstance(c, S3FileClient) + self.assertFalse(self.collector.has_errors()) + + def test_init_bucket_not_found(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.side_effect = module.ClientError( + {"Error": {"Code": "404"}}, "HeadBucket" + ) + + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_bucket_forbidden(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.side_effect = module.ClientError( + {"Error": {"Code": "403"}}, "HeadBucket" + ) + + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_no_credentials(self): + with patch("boto3.Session", side_effect=module.NoCredentialsError()): + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_generic_error(self): + with patch("boto3.Session", side_effect=Exception("boom")): + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_read_json_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = { + "Body": BytesIO(json.dumps({"a": 1}).encode()) + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), {"a": 1}) + + def test_read_json_empty(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = {"Body": BytesIO(b"")} + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + + def test_read_json_invalid(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = {"Body": BytesIO(b"{bad json")} + + c = S3FileClient("bucket", error_collector=self.collector) + c.read_json("x.json") + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_json_boto_error(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.side_effect = module.ClientError( + {"Error": {"Code": "NoSuchKey"}}, "GetObject" + ) + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_json_generic_error(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.side_effect = RuntimeError("boom") + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_jsonl_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = { + "Body": BytesIO(b'{"x":1}\n{"y":2}') + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + c.read_jsonl("x.jsonl"), + [{"x": 1}, {"y": 2}], + ) + + def test_read_csv_success(self): + mock_s3 = self._mock_boto_session() + csv_data = "a,b\n1,2\n3,4" + mock_s3.get_object.return_value = { + "Body": BytesIO(csv_data.encode()) + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + c.read_csv("x.csv"), + [{"a": "1", "b": "2"}, {"a": "3", "b": "4"}], + ) + + def test_upload_json_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.put_object.return_value = True + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertTrue(c.upload_json({"a": 1}, "x.json")) + + def test_upload_json_failure(self): + mock_s3 = self._mock_boto_session() + mock_s3.put_object.side_effect = Exception("boom") + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertFalse(c.upload_json({"a": 1}, "x.json")) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.FILE_UPLOAD_ERROR, + ) + + def test_get_sqlitedb_tables_data_from_s3(self): + mock_s3 = self._mock_boto_session() + + tmp = tempfile.NamedTemporaryFile(delete=False) + conn = sqlite3.connect(tmp.name) + conn.execute("CREATE TABLE t (id INTEGER, val TEXT)") + conn.execute("INSERT INTO t VALUES (1,'A')") + conn.commit() + conn.close() + + with open(tmp.name, "rb") as f: + db_bytes = f.read() + + mock_s3.download_fileobj.side_effect = ( + lambda b, k, fd: fd.write(db_bytes) + ) + + c = S3FileClient("bucket", error_collector=self.collector) + + out = c.get_sqlitedb_tables_data_from_s3("db.sqlite", ["t"]) + + self.assertEqual( + out["t"], + [{"id": 1, "val": "A"}], + ) + + +import json +import sqlite3 +import tempfile +import unittest +from unittest.mock import MagicMock, patch +from io import BytesIO + +import gen_ai_hub.evaluations.helpers.s3_file_client as module +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode +from gen_ai_hub.evaluations.helpers.s3_file_client import S3FileClient + + + +class TestS3FileClient(unittest.TestCase): + + def setUp(self): + self.collector = ValidationCollector() + + def _mock_boto_session(self): + mock_s3 = MagicMock() + + patcher = patch("boto3.Session") + self.addCleanup(patcher.stop) + mock_session = patcher.start() + + session_obj = MagicMock() + session_obj.client.return_value = mock_s3 + session_obj.region_name = "us-east-1" + mock_session.return_value = session_obj + + return mock_s3 + + def test_init_valid_bucket(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.return_value = True + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertIsInstance(c, S3FileClient) + self.assertFalse(self.collector.has_errors()) + + def test_init_bucket_not_found(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.side_effect = module.ClientError( + {"Error": {"Code": "404"}}, "HeadBucket" + ) + + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_bucket_forbidden(self): + mock_s3 = self._mock_boto_session() + mock_s3.head_bucket.side_effect = module.ClientError( + {"Error": {"Code": "403"}}, "HeadBucket" + ) + + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_no_credentials(self): + with patch("boto3.Session", side_effect=module.NoCredentialsError()): + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_init_generic_error(self): + with patch("boto3.Session", side_effect=Exception("boom")): + S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.INVALID_S3_CLIENT_ERROR, + ) + + def test_read_json_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = { + "Body": BytesIO(json.dumps({"a": 1}).encode()) + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), {"a": 1}) + + def test_read_json_empty(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = {"Body": BytesIO(b"")} + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + + def test_read_json_invalid(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = {"Body": BytesIO(b"{bad json")} + + c = S3FileClient("bucket", error_collector=self.collector) + c.read_json("x.json") + + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_json_boto_error(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.side_effect = module.ClientError( + {"Error": {"Code": "NoSuchKey"}}, "GetObject" + ) + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_json_generic_error(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.side_effect = RuntimeError("boom") + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_json("x.json"), []) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_read_jsonl_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = { + "Body": BytesIO(b'{"x":1}\n{"y":2}') + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + c.read_jsonl("x.jsonl"), + [{"x": 1}, {"y": 2}], + ) + + def test_read_jsonl_invalid_lines(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = { + "Body": BytesIO(b'{"x":1}\nbad\n{"y":2}') + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + c.read_jsonl("x.jsonl"), + [{"x": 1}, {"y": 2}], + ) + + def test_read_csv_success(self): + mock_s3 = self._mock_boto_session() + csv_data = "a,b\n1,2\n3,4" + mock_s3.get_object.return_value = { + "Body": BytesIO(csv_data.encode()) + } + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual( + c.read_csv("x.csv"), + [{"a": "1", "b": "2"}, {"a": "3", "b": "4"}], + ) + + def test_read_csv_parsing_error(self): + mock_s3 = self._mock_boto_session() + mock_s3.get_object.return_value = {"Body": BytesIO(b"bad,data")} + + with patch("pandas.read_csv", side_effect=module.csv.Error("bad csv")): + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertEqual(c.read_csv("x.csv"), []) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.READ_FILE_DATA_FROM_ARTIFACT_ERROR, + ) + + def test_upload_json_success(self): + mock_s3 = self._mock_boto_session() + mock_s3.put_object.return_value = True + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertTrue(c.upload_json({"a": 1}, "x.json")) + + def test_upload_json_failure(self): + mock_s3 = self._mock_boto_session() + mock_s3.put_object.side_effect = Exception("boom") + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertFalse(c.upload_json({"a": 1}, "x.json")) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.FILE_UPLOAD_ERROR, + ) + + def test_upload_csv_empty_data(self): + mock_s3 = self._mock_boto_session() + + c = S3FileClient("bucket", error_collector=self.collector) + + self.assertFalse(c.upload_csv([], "x.csv")) + self.assertEqual( + self.collector.errors[0][0], + ErrorCode.FILE_UPLOAD_ERROR, + ) + + def test_get_sqlitedb_tables_data_from_s3(self): + mock_s3 = self._mock_boto_session() + + tmp = tempfile.NamedTemporaryFile(delete=False) + conn = sqlite3.connect(tmp.name) + conn.execute("CREATE TABLE t (id INTEGER, val TEXT)") + conn.execute("INSERT INTO t VALUES (1,'A')") + conn.commit() + conn.close() + + with open(tmp.name, "rb") as f: + db_bytes = f.read() + + mock_s3.download_fileobj.side_effect = ( + lambda b, k, fd: fd.write(db_bytes) + ) + + c = S3FileClient("bucket", error_collector=self.collector) + + out = c.get_sqlitedb_tables_data_from_s3("db.sqlite", ["t"]) + + self.assertEqual( + out["t"], + [{"id": 1, "val": "A"}], + ) diff --git a/packages/gen/tests/evaluations/test_validation_utils.py b/packages/gen/tests/evaluations/test_validation_utils.py new file mode 100644 index 0000000..c7614be --- /dev/null +++ b/packages/gen/tests/evaluations/test_validation_utils.py @@ -0,0 +1,1539 @@ +import unittest +from unittest.mock import patch, MagicMock + +from gen_ai_hub.evaluations.helpers.collector import ValidationCollector +from gen_ai_hub.evaluations.constants import ( + ALL_METRICS_COLUMN_MAPPING_KEY, + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, +) +from gen_ai_hub.evaluations._internal._models import _EvaluationConfigData +from gen_ai_hub.evaluations.exceptions.error_codes import ErrorCode + +from gen_ai_hub.evaluations.utils.validation_utils import ( + validate_metrics, + validate_orchestration_url_across_configs, + validate_orchestration_url, + validate_orchestration_configuration, + validate_input_config, + validate_variable_mapping_of_prompts, + validate_variable_mapping_of_metrics, + remove_filter_metrics_if_provider_not_supported, + validate_filtered_models, + _validate_allowed_models, + _validate_denied_models, + _is_model_version_allowed, + _is_model_version_denied, + fetch_and_validate_orchestration_config, + validate_variable_mapping_with_input_config, + validate_config_data_collection, + validate_merged_config_data, +) + +from gen_ai_hub.evaluations.utils.orch_config_utils import ( + validate_if_all_grounding_input_params_present_in_prompt_variables, +) + +class TestValidateMetrics(unittest.TestCase): + + def test_metrics_empty_list(self): + collector = ValidationCollector() + metrics = [] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + with self.assertRaisesRegex( + RuntimeError, + "Metrics list cannot be empty. Atleast one metric needs to be provided", + ): + collector.raise_if_errors() + + def test_metrics_not_empty_but_one_metric_is_empty_string(self): + collector = ValidationCollector() + metrics = ["bleu", "", "bert_score"] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + with self.assertRaisesRegex( + RuntimeError, + "Metric name cannot be empty. Please provide a valid metric name", + ): + collector.raise_if_errors() + + def test_metrics_list_with_empty_metric_name(self): + collector = ValidationCollector() + metrics = [""] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + with self.assertRaisesRegex( + RuntimeError, + "Metric name cannot be empty. Please provide a valid metric name", + ): + collector.raise_if_errors() + + def test_metrics_list_with_missing_metric_name(self): + collector = ValidationCollector() + metrics = ["bleu", "f1/score", "bert_score"] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + with self.assertRaisesRegex( + RuntimeError, + "f1/score is neither a system supported metric nor provided in metric templates", + ): + collector.raise_if_errors() + + def test_metrics_valid_system_supported_metrics(self): + collector = ValidationCollector() + metrics = ["BERT Score", "BLEU"] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + # Should not raise + collector.raise_if_errors() + + def test_metrics_valid_custom_metrics(self): + collector = ValidationCollector() + metrics = ["custom_metric_1", "custom_metric_2"] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + metric_templates = [ + { + "id": "custom_metric_1", + "name": "groundedness", + "spec": { + "promptType": "free-form", + "configuration": { + "modelConfiguration": { + "name": "gpt-4o", + "version": "2024-08-06", + "parameters": [], + }, + "promptConfiguration": { + "systemPrompt": "system prompt", + "userPrompt": "user prompt", + "dataType": "numeric", + }, + }, + }, + }, + { + "id": "custom_metric_2", + "name": "custom_metric_1", + "spec": { + "promptType": "free-form", + "configuration": { + "modelConfiguration": { + "name": "gpt-4o", + "version": "2024-08-06", + "parameters": [], + }, + "promptConfiguration": { + "systemPrompt": "system prompt", + "userPrompt": "user prompt", + "dataType": "numeric", + }, + }, + }, + }, + ] + + validate_metrics(metrics, metric_templates, orchestration_config_data, collector) + + # Should not raise + collector.raise_if_errors() + + def test_metrics_list_with_rag_flag_enabled(self): + collector = ValidationCollector() + metrics = ["BLEU", "BERT Score"] + orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + + validate_metrics(metrics, [], orchestration_config_data, collector) + + # Should not raise + collector.raise_if_errors() + + + + + +class TestValidateOrchestrationUrl(unittest.TestCase): + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.update_test_orch_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.extract_deployment_id") + def test_valid_orch_url( + self, + mock_extract_deployment_id, + mock_fetch_deployment_config, + mock_fetch_and_validate, + mock_select_model, + mock_update_test_config, + mock_call_orch_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "cfg-123" + + mock_extract_deployment_id.return_value = "deployment-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_config.return_value = {"test": "config"} + + evaluation_config_data = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url( + evaluation_config_data, + "https://host/v2/inference/deployments/valid/", + mock_ai_core_client, + "resource-group", + collector, + ) + + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.extract_deployment_id") + def test_invalid_status_raises_validation_error( + self, + mock_extract_deployment_id, + mock_fetch_deployment_config, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.DEAD + mock_deployment_config.configuration_id = "cfg-123" + + mock_extract_deployment_id.return_value = "deployment-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + + evaluation_config_data = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url( + evaluation_config_data, + "https://host/v2/inference/deployments/invalid/", + mock_ai_core_client, + "resource-group", + collector, + ) + + with self.assertRaisesRegex( + RuntimeError, + "Deployment status is.*expected 'RUNNING'", + ): + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.update_test_orch_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.extract_deployment_id") + def test_json_decode_error_handled( + self, + mock_extract_deployment_id, + mock_fetch_deployment_config, + mock_fetch_and_validate, + mock_select_model, + mock_update_test_config, + mock_call_orch_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "cfg-123" + + mock_extract_deployment_id.return_value = "deployment-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_config.return_value = {"test": "config"} + + def side_effect(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get("error_collector") + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Expecting value", + ) + + mock_call_orch_service.side_effect = side_effect + + evaluation_config_data = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url( + evaluation_config_data, + "https://host/v2/inference/deployments/jsondecode/", + mock_ai_core_client, + "resource-group", + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_orchestration_url") + def test_validate_orch_url_across_configs_single_item( + self, + mock_validate_orch_url, + ): + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + config = _EvaluationConfigData( + orch_config_data=[{"modules": {}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url_across_configs( + accumulated_config_data=config, + orchestration_url="https://host/v2/inference/deployments/valid/", + ai_core_client=mock_ai_core_client, + resource_group="resource-group", + error_collector=collector, + ) + + mock_validate_orch_url.assert_called_once_with( + config, + "https://host/v2/inference/deployments/valid/", + mock_ai_core_client, + "resource-group", + collector, + None, + ) + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_orchestration_url") + def test_validate_orch_url_across_configs_list( + self, + mock_validate_orch_url, + ): + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + config2 = _EvaluationConfigData( + orch_config_data=[{"modules": {}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url_across_configs( + accumulated_config_data=[config1, config2], + orchestration_url="https://host/v2/inference/deployments/valid/", + ai_core_client=mock_ai_core_client, + resource_group="resource-group", + error_collector=collector, + ) + + self.assertEqual(mock_validate_orch_url.call_count, 2) + + mock_validate_orch_url.assert_any_call( + config1, + "https://host/v2/inference/deployments/valid/", + mock_ai_core_client, + "resource-group", + collector, + None, + ) + + mock_validate_orch_url.assert_any_call( + config2, + "https://host/v2/inference/deployments/valid/", + mock_ai_core_client, + "resource-group", + collector, + None, + ) + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.update_test_orch_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.extract_deployment_id") + def test_retry_exception_handled_as_success( + self, + mock_extract_deployment_id, + mock_fetch_deployment_config, + mock_fetch_and_validate, + mock_select_model, + mock_update_test_config, + mock_call_orch_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "cfg-123" + + mock_extract_deployment_id.return_value = "deployment-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_config.return_value = {"test": "config"} + mock_call_orch_service.return_value = None + + evaluation_config_data = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url( + evaluation_config_data, + "https://host/v2/inference/deployments/retry/", + mock_ai_core_client, + "resource-group", + collector, + ) + + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.call_orchestration_service_with_v2_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.update_test_orch_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.select_model_details_randomly") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_and_validate_orchestration_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_deployment_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.extract_deployment_id") + def test_unexpected_exception_raises_validation_error( + self, + mock_extract_deployment_id, + mock_fetch_deployment_config, + mock_fetch_and_validate, + mock_select_model, + mock_update_test_config, + mock_call_orch_service, + ): + from ai_api_client_sdk.models.status import Status + from ai_core_sdk.ai_core_v2_client import AICoreV2Client + + collector = ValidationCollector() + mock_ai_core_client = MagicMock(spec=AICoreV2Client) + + mock_deployment_config = MagicMock() + mock_deployment_config.status = Status.RUNNING + mock_deployment_config.configuration_id = "cfg-123" + + mock_extract_deployment_id.return_value = "deployment-123" + mock_fetch_deployment_config.return_value = mock_deployment_config + mock_select_model.return_value = ("gpt-4", "4.0") + mock_update_test_config.return_value = {"test": "config"} + + def side_effect(*args, **kwargs): + error_collector = args[4] if len(args) > 4 else kwargs.get("error_collector") + error_collector.add_error( + ErrorCode.INVALID_ORCHESTRATION_CONFIG_ERROR, + "Error occurred: Unexpected error", + ) + + mock_call_orch_service.side_effect = side_effect + + evaluation_config_data = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_orchestration_url( + evaluation_config_data, + "https://host/v2/inference/deployments/unexpected/", + mock_ai_core_client, + "resource-group", + collector, + ) + + with self.assertRaisesRegex(RuntimeError, "Unexpected error"): + collector.raise_if_errors() + + + + + +class TestValidateConfigDataCollection(unittest.TestCase): + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_merged_config_data") + def test_validate_config_data_collection_single_item(self, mock_validate_merged): + """Test validate_config_data_collection with single _EvaluationConfigData""" + collector = ValidationCollector() + + config = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_config_data_collection(config, collector) + + mock_validate_merged.assert_called_once_with(config, collector) + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_merged_config_data") + def test_validate_config_data_collection_list(self, mock_validate_merged): + """Test validate_config_data_collection with list""" + collector = ValidationCollector() + + config1 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + config2 = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-3"}}}}], + dataset_type="json", + dataset_data={}, + metric_templates=[], + metrics_list=[], + ) + + validate_config_data_collection([config1, config2], collector) + + self.assertEqual(mock_validate_merged.call_count, 2) + mock_validate_merged.assert_any_call(config1, collector) + mock_validate_merged.assert_any_call(config2, collector) + + +class TestValidateMergedConfigData(unittest.TestCase): + + @patch("gen_ai_hub.evaluations.utils.validation_utils.handle_reference_missing_rows") + @patch("gen_ai_hub.evaluations.utils.validation_utils.handle_language_match") + @patch("gen_ai_hub.evaluations.utils.validation_utils.handle_json_schema_match") + @patch("gen_ai_hub.evaluations.utils.validation_utils.handle_missing_dependent_variables_in_dataset") + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_variable_mapping_with_input_config") + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_input_config") + def test_validate_merged_config_data( + self, + mock_validate_input_config, + mock_validate_variable_mapping, + mock_handle_missing_dependent, + mock_handle_json_schema, + mock_handle_language, + mock_handle_reference, + ): + """Test validate_merged_config_data calls all validation functions""" + + collector = ValidationCollector() + + config = _EvaluationConfigData( + orch_config_data=[{"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}}], + dataset_type="json", + dataset_data={"row1": {"input": "test"}}, + metric_templates=[{"id": "metric1", "name": "metric1"}], + metrics_list=["metric1"], + variable_mapping={"prompt/input": "data/input"}, + ) + + validate_merged_config_data(config, collector) + + mock_validate_input_config.assert_called_once_with( + config.orch_config_data, + config.metrics_list, + config.metric_templates, + collector, + ) + + mock_validate_variable_mapping.assert_called_once_with( + config.orch_config_data, + config.dataset_data, + config.variable_mapping, + config.metrics_list, + config.metric_templates, + collector, + ) + + mock_handle_missing_dependent.assert_called_once_with( + config.dataset_data, + config.metrics_list, + config.metric_templates, + config.variable_mapping, + collector, + ) + + mock_handle_json_schema.assert_called_once_with( + config.metrics_list, + config.dataset_data, + config.variable_mapping, + collector, + ) + + mock_handle_language.assert_called_once_with( + config.metrics_list, + config.dataset_data, + config.variable_mapping, + collector, + ) + + mock_handle_reference.assert_called_once_with( + config.dataset_data, + config.variable_mapping, + config.metrics_list, + collector, + ) + + + +class TestValidateOrchestrationConfiguration(unittest.TestCase): + + def test_missing_modules(self): + collector = ValidationCollector() + orchestration_config_data = [{}] + + with self.assertRaisesRegex(RuntimeError, "modules is mandatory"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_missing_llm_module_config_or_templating_module_config(self): + collector = ValidationCollector() + orchestration_config_data = [{"modules": {"prompt_templating": {}}}] + + with self.assertRaisesRegex(RuntimeError, "Missing inside here configuration"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_missing_model_name_in_llm_module_config(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {}, + "prompt": {"template": [{"content": "some content", "role": "user"}]}, + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "Missing configuration for.*name"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_template_ref_not_supported(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": {"template_ref": "uuid"}, + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "template_ref inside prompt is not yet supported"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_empty_template_list(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": {"template": []}, + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "template list cannot be empty"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_missing_content_in_template(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": {"template": [{}]}, + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "Each template must be a dictionary"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_image_url_not_supported(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": { + "template": [{ + "role": "user", + "content": [{ + "type": "image_url", + "image_url": {"url": "https://image.com"} + }] + }] + } + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "image_url is not supported"): + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + def test_positive_case(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": { + "template": [{ + "content": "Prompt {{?var1}} {{?var2}}", + "role": "user", + }] + } + } + } + }] + + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() # Should not raise + + def test_grounding_output_missing(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"role": "user", "content": "Test {{?otherVar}}"}] + }, + "model": {"name": "gpt-4"} + }, + "grounding": { + "config": {"placeholders": {"output": "groundingResult"}} + }, + } + }] + + with self.assertRaises(RuntimeError) as context: + validate_orchestration_configuration(orchestration_config_data, collector) + collector.raise_if_errors() + + self.assertIn(ErrorCode.INVALID_GROUNDING_CONFIGURATION.value, str(context.exception)) + self.assertIn("groundingResult", str(context.exception)) + + def test_content_filter_supported_provider(self): + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "prompt": {"template": [{"role": "user", "content": "Hello"}]}, + "model": {"name": "gpt-4"}, + }, + "filtering": { + "input": {"filters": [{"type": "azure_content_safety"}]}, + "output": {"filters": [{"type": "azure_content_safety"}]}, + }, + } + }] + + metrics = [ + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, + "other_metric" + ] + + collector = ValidationCollector() + + remove_filter_metrics_if_provider_not_supported( + orchestration_config_data, metrics, collector + ) + + self.assertEqual(len(metrics), 3) + + def test_content_filter_unsupported_provider(self): + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "prompt": {"template": [{"role": "user", "content": "Hello"}]}, + "model": {"name": "gpt-4"}, + }, + "filtering": { + "input": {"filters": [{"type": "unsupported_provider"}]}, + "output": {"filters": [{"type": "unsupported_provider"}]}, + }, + } + }] + + metrics = [ + CONTENT_FILTER_ON_INPUT_METRIC_ID, + CONTENT_FILTER_ON_OUTPUT_METRIC_ID, + "other_metric_1", + "other_metric_2", + ] + + collector = ValidationCollector() + + remove_filter_metrics_if_provider_not_supported( + orchestration_config_data, metrics, collector + ) + + self.assertNotIn(CONTENT_FILTER_ON_INPUT_METRIC_ID, metrics) + self.assertNotIn(CONTENT_FILTER_ON_OUTPUT_METRIC_ID, metrics) + self.assertIn("other_metric_1", metrics) + self.assertIn("other_metric_2", metrics) + self.assertEqual(len(metrics), 2) + + +class TestValidateInputParameters(unittest.TestCase): + + def test_empty_metrics(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": {"template": [{"content": "text", "role": "user"}]}, + } + } + }] + + with self.assertRaisesRegex(RuntimeError, "Metrics list cannot be empty"): + validate_input_config(orchestration_config_data, [], [], collector) + collector.raise_if_errors() + + def test_valid_input_parameters(self): + collector = ValidationCollector() + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "latest"}, + "prompt": {"template": [{"content": "text", "role": "user"}]}, + } + } + }] + + metrics = ["bert_score"] + + metric_templates = [{ + "id": "bert_score", + "name": "BERT Score", + "evaluation_method": "computed", + }] + + validate_input_config( + orchestration_config_data, + metrics, + metric_templates, + collector, + ) + + collector.raise_if_errors() # Should not raise + + +class TestValidateVariableMappingOfMetrics(unittest.TestCase): + + def test_valid_all_metrics_reference_mapping(self): + collector = ValidationCollector() + metrics = ["bleu", "bert_score"] + template_vars_data = [{"reference": "value1", "column1": "value2"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/reference" + } + + validate_variable_mapping_of_metrics( + metrics, + [], + template_vars_data, + variable_mapping, + collector, + ) + + collector.raise_if_errors() + + def test_invalid_all_metrics_reference_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference-key": "value1", "column1": "value2"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/invalid_column" + } + + validate_variable_mapping_of_metrics( + metrics, + [], + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_valid_individual_metrics_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference": "value1", "json_schema": "value2"}] + variable_mapping = { + "metric1/reference": "data/reference", + "metric2/json_schema": "data/json_schema", + } + + metric_templates = [ + { + "id": "metric1", + "name": "Metric 1", + "additionalProperties": {"variables": ["reference"]}, + }, + { + "id": "metric2", + "name": "Metric 2", + "additionalProperties": {"variables": ["json_schema"]}, + }, + ] + + validate_variable_mapping_of_metrics( + metrics, + metric_templates, + template_vars_data, + variable_mapping, + collector, + ) + + collector.raise_if_errors() + + def test_invalid_individual_metrics_mapping(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"reference": "value1", "json_schema": "value2"}] + variable_mapping = { + "metric1/invalid_key": "data/reference", + "metric2/json_schema": "data/invalid_column", + } + + validate_variable_mapping_of_metrics( + metrics, + [], + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_missing_dataset_column_for_all_metrics(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"column1": "value1"}] + variable_mapping = { + f"{ALL_METRICS_COLUMN_MAPPING_KEY}/reference": "data/reference" + } + + validate_variable_mapping_of_metrics( + metrics, + [], + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_missing_dataset_column_for_individual_metrics(self): + collector = ValidationCollector() + metrics = ["metric1", "metric2"] + template_vars_data = [{"column1": "value1"}] + variable_mapping = { + "metric1/reference": "data/reference", + "metric2/json_schema": "data/json_schema", + } + + validate_variable_mapping_of_metrics( + metrics, + [], + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + +class TestValidateVariableMappingOfPrompts(unittest.TestCase): + + def test_valid_variable_mapping(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "This is a prompt with {{?var1}} and {{?var2}}.", "role": "user"} + ] + } + }} + }] + + template_vars_data = [{"var1": "value1", "var2": "value2"}] + variable_mapping = { + "prompt/var1": "data/var1", + "prompt/var2": "data/var2", + } + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + collector.raise_if_errors() + + def test_missing_variable_in_mapping_and_dataset(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "This is a prompt with {{?var1}} and {{?var2}}.", "role": "user"} + ] + } + }} + }] + + template_vars_data = [{"var1": "value1"}] + variable_mapping = {"prompt/var1": "data/var1"} + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_system_defined_variable_in_prompt(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "This is a prompt with {{?prompt}}", "role": "user"} + ] + } + }} + }] + + validate_variable_mapping_of_prompts( + orchestration_config_data, + [{"var1": "value1"}], + {}, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_variable_in_dataset_but_not_in_mapping(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "This is a prompt with {{?var1}} and {{?var2}}.", "role": "user"} + ] + } + }} + }] + + template_vars_data = [{"var1": "value1", "var2": "value2"}] + variable_mapping = {"prompt/var1": "data/var1"} + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + collector.raise_if_errors() + + def test_variable_in_mapping_but_not_in_dataset(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "This is a prompt with {{?var1}} and {{?var2}}.", "role": "user"} + ] + } + }} + }] + + template_vars_data = [{"var1": "value1"}] + variable_mapping = { + "prompt/var1": "data/var1", + "prompt/var2": "data/var2", + } + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_variable_with_defaults_skipped_validation(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "Write about {{?topic}} in {{?sentiment}} way", "role": "user"} + ], + "defaults": {"topic": "apple"}, + } + }} + }] + + template_vars_data = [{"sentiment": "positive"}] + variable_mapping = {"prompt/sentiment": "data/sentiment"} + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + collector.raise_if_errors() + + def test_variable_without_defaults_requires_validation(self): + collector = ValidationCollector() + + orchestration_config_data = [{ + "modules": {"prompt_templating": { + "prompt": { + "template": [ + {"content": "Write about {{?topic}} in {{?sentiment}} way", "role": "user"} + ], + "defaults": {"topic": "apple"}, + } + }} + }] + + template_vars_data = [{"topic": "banana"}] + variable_mapping = {"prompt/topic": "data/topic"} + + validate_variable_mapping_of_prompts( + orchestration_config_data, + template_vars_data, + variable_mapping, + collector, + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + + + +class TestValidateIfGroundingInputPresentInPromptVariables(unittest.TestCase): + + def test_missing_modules_key(self): + orch_config = {} + collector = ValidationCollector() + + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, collector + ) + + self.assertFalse(collector.has_errors()) + + def test_missing_grounding_module_config_key(self): + orch_config = {"modules": {}} + collector = ValidationCollector() + + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, collector + ) + + self.assertFalse(collector.has_errors()) + + def test_empty_input_params_list(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"content": "Test {{?var1}}", "role": "user"}] + } + }, + "grounding": {"config": {"placeholders": {"input": []}}}, + } + } + + collector = ValidationCollector() + + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, collector + ) + + self.assertFalse(collector.has_errors()) + + def test_valid_input_params(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"content": "Test {{?var1}} and {{?var2}}", "role": "user"}] + } + }, + "grounding": { + "config": {"placeholders": {"input": ["var1", "var2"]}} + }, + } + } + + collector = ValidationCollector() + + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, collector + ) + + self.assertFalse(collector.has_errors()) + + def test_missing_input_params(self): + orch_config = { + "modules": { + "prompt_templating": { + "prompt": { + "template": [{"content": "Test {{?var1}}", "role": "user"}] + } + }, + "grounding": { + "config": {"placeholders": {"input": ["var1", "var2"]}} + }, + } + } + + collector = ValidationCollector() + + validate_if_all_grounding_input_params_present_in_prompt_variables( + orch_config, collector + ) + + errors = collector.errors + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0][0], ErrorCode.INVALID_GROUNDING_CONFIGURATION.value) + self.assertIn("var2", errors[0][1]) + + + +class TestValidateFilteredModels(unittest.TestCase): + + def setUp(self): + self.valid_run_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "4.0"} + } + } + }] + + def test_allow_filter_allows_valid_model(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = '[{"modelName": "gpt-4", "modelVersions": ["4.0"]}]' + + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "allow" + + collector = ValidationCollector() + + validate_filtered_models([param1, param2], self.valid_run_data, collector) + collector.raise_if_errors() + + def test_deny_filter_blocks_invalid_model(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = '[{"modelName": "gpt-4", "modelVersions": ["4.0"]}]' + + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "deny" + + collector = ValidationCollector() + + validate_filtered_models([param1, param2], self.valid_run_data, collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_invalid_filter_type_adds_error(self): + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = '[{"modelName": "gpt-4", "modelVersions": ["4.0"]}]' + + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "invalid" + + collector = ValidationCollector() + + validate_filtered_models([param1, param2], self.valid_run_data, collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_no_model_filter_list_skips_filtering(self): + collector = ValidationCollector() + validate_filtered_models([], self.valid_run_data, collector) + collector.raise_if_errors() + + def test_validate_allow_filter_blocks_unknown_model(self): + collector = ValidationCollector() + run_models = {"gpt-4": ["4.0"], "llama": ["7b"]} + allowed_models = {"gpt-4": ["4.0"]} + + _validate_allowed_models(run_models, allowed_models, collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_validate_deny_filter_allows_unlisted_models(self): + collector = ValidationCollector() + run_models = {"gpt-4": ["4.0"]} + denied_models = {"llama": ["7b"]} + + _validate_denied_models(run_models, denied_models, collector) + collector.raise_if_errors() + + def test_validate_deny_filter_blocks_if_listed(self): + collector = ValidationCollector() + run_models = {"gpt-4": ["4.0"]} + denied_models = {"gpt-4": ["4.0"]} + + _validate_denied_models(run_models, denied_models, collector) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + def test_is_model_version_allowed_true(self): + self.assertTrue( + _is_model_version_allowed("gpt-4", "4.0", {"gpt-4": ["4.0", "4.1"]}) + ) + + def test_is_model_version_allowed_false(self): + self.assertFalse( + _is_model_version_allowed("gpt-4", "3.5", {"gpt-4": ["4.0"]}) + ) + + def test_is_model_version_denied_true(self): + self.assertTrue( + _is_model_version_denied("gpt-4", "4.0", {"gpt-4": ["4.0", "4.1"]}) + ) + + def test_is_model_version_denied_false(self): + self.assertFalse( + _is_model_version_denied("gpt-4", "3.5", {"gpt-4": ["4.0"]}) + ) + + + +class TestFetchAndValidateOrchConfig(unittest.TestCase): + + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_configuration_by_id") + def test_fetch_and_validate_valid_allowlist(self, mock_fetch): + mock_config_response = MagicMock() + + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = '[{"modelName": "gpt-4", "modelVersions": ["4.0"]}]' + + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "allow" + + mock_config_response.parameter_bindings = [param1, param2] + mock_fetch.return_value = mock_config_response + + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "4.0"} + } + } + }] + + collector = ValidationCollector() + + fetch_and_validate_orchestration_config( + MagicMock(), "config-123", orchestration_config_data, "rg", collector + ) + + collector.raise_if_errors() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.fetch_configuration_by_id") + def test_fetch_and_validate_invalid_filter(self, mock_fetch): + mock_config_response = MagicMock() + + param1 = MagicMock() + param1.key = "modelFilterList" + param1.value = '[{"modelName": "gpt-4", "modelVersions": ["4.0"]}]' + + param2 = MagicMock() + param2.key = "modelFilterListType" + param2.value = "unsupported" + + mock_config_response.parameter_bindings = [param1, param2] + mock_fetch.return_value = mock_config_response + + orchestration_config_data = [{ + "modules": { + "prompt_templating": { + "model": {"name": "gpt-4", "version": "4.0"} + } + } + }] + + collector = ValidationCollector() + + fetch_and_validate_orchestration_config( + MagicMock(), "config-xyz", orchestration_config_data, "rg", collector + ) + + with self.assertRaises(RuntimeError): + collector.raise_if_errors() + + + +class TestValidateVariableMappingWithInputConfig(unittest.TestCase): + + def setUp(self): + self.orchestration_config_data = [ + {"modules": {"prompt_templating": {"model": {"name": "gpt-4"}}}} + ] + self.template_vars_data = [{"input": "Hello"}] + self.variable_mapping = {"all_metrics/input": "input"} + self.metrics = ["some.metric"] + self.collector = ValidationCollector() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_variable_mapping_of_metrics") + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_variable_mapping_of_prompts") + def test_calls_all_validation_subfunctions( + self, + mock_validate_prompts, + mock_validate_metrics, + ): + validate_variable_mapping_with_input_config( + self.orchestration_config_data, + self.template_vars_data, + self.variable_mapping, + self.metrics, + [], + self.collector, + ) + + mock_validate_prompts.assert_called_once() + mock_validate_metrics.assert_called_once() + + @patch("gen_ai_hub.evaluations.utils.validation_utils.validate_variable_mapping_of_prompts") + def test_raises_if_any_child_fails(self, mock_validate_prompts): + mock_validate_prompts.side_effect = RuntimeError("Prompt validation failed") + + with self.assertRaises(RuntimeError): + validate_variable_mapping_with_input_config( + self.orchestration_config_data, + self.template_vars_data, + self.variable_mapping, + self.metrics, + [], + self.collector, + ) diff --git a/packages/gen/tests/mock.py b/packages/gen/tests/mock.py new file mode 100644 index 0000000..01e98d3 --- /dev/null +++ b/packages/gen/tests/mock.py @@ -0,0 +1,2362 @@ +from __future__ import annotations + +import asyncio +import json +import os +import pathlib +from contextlib import contextmanager, asynccontextmanager +from typing import Any, Dict, Final, List, Tuple, Type + +import numpy as np +import requests_mock +import respx +from httpx import Response, AsyncByteStream + +from gen_ai_hub.prompt_registry.models.prompt_template import (PromptTemplateSpec, PromptTemplateListResponse, + PromptTemplateGetResponse, PromptTemplatePostResponse, + PromptTemplateDeleteResponse, PromptTemplate, + PromptTemplateSubstitutionRequest, + PromptTemplateSubstitutionResponse) +from gen_ai_hub.proxy.core.base import BaseDeployment, BaseProxyClient +from gen_ai_hub.proxy.core.proxy_clients import proxy_clients + +PREFIX: Final[str] = 'MOCK_LLM' +MOCK_LLM_DEFAULT_HOME: Final[str] = pathlib.Path('~/.mock_llm').expanduser().__str__() + + +class MockDeployment(BaseDeployment): + url: str + + # abstractmethod implementations + def additional_request_body_kwargs(self) -> Dict[str, Any]: + return {} + + @staticmethod + def get_model_identification_kwargs() -> Tuple[str]: + return ('a', 'b', 'c') + + @property + def prediction_url(self): + return self.url + '/predict' + + +@proxy_clients.register('mock') +class MockProxyClient(BaseProxyClient): + url: str = 'mock_url' + token: str = 'mock_token' + + @property + def request_header(self) -> Dict[str, Any]: + return {'token': self.token} + + @property + def deployments(self) -> List[MockDeployment]: + return [MockDeployment(url=self.url)] + + @property + def deployment_class(self) -> Type[MockDeployment]: + return MockDeployment + + def select_deployment(self) -> MockDeployment: + return self.deployments[0] + + @classmethod + def get_home(cls): + return pathlib.Path(os.environ.get(f'{PREFIX}_HOME', MOCK_LLM_DEFAULT_HOME)).expanduser() + + +# {{auth_url}}/oauth/token +GET_TOKEN_RESPONSE = {'access_token': 'xxx', 'token_type': 'bearer', 'expires_in': 43199, 'scope': '???'} + +# {{apiurl}}/v2/lm/deployments +GET_DEPLOYMENTS_RESPONSE = { + 'count': + 7, + 'resources': [ + { + 'configurationId': 'ad4a2c61-875a-416a-96b6-24ed1347a164', + 'configurationName': 'gpt-4o-mini', + 'createdAt': '2023-11-17T13:13:29Z', + 'deploymentUrl': + 'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d768512472eae8f4', + 'details': { + 'resources': { + 'backend_details': { + 'model': { + 'name': 'gpt-4o-mini', + 'version': 'latest' + } + } + }, + 'scaling': { + 'backend_details': {} + } + }, + 'id': 'd768512472eae8f4', + 'lastOperation': 'CREATE', + 'latestRunningConfigurationId': 'ad4a2c61-875a-416a-96b6-24ed1347a164', + 'modifiedAt': '2023-12-29T21:16:45Z', + 'scenarioId': 'foundation-models', + 'startTime': '2023-11-17T13:17:03Z', + 'status': 'RUNNING', + 'submissionTime': '2023-11-17T13:15:19Z', + 'targetStatus': 'RUNNING' + }, + { + 'configurationId': 'e09e58c3-15bf-42ad-8a5e-5b3273846dda', + 'configurationName': 'text-embedding-ada-002-latest', + 'createdAt': '2023-11-20T11:24:09Z', + 'deploymentUrl': + 'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dac9dca90be5213a', + 'details': { + 'resources': { + 'backend_details': { + 'model': { + 'name': 'text-embedding-ada-002', + 'version': 'latest' + } + } + }, + 'scaling': { + 'backend_details': {} + } + }, + 'id': 'dac9dca90be5213a', + 'lastOperation': 'CREATE', + 'latestRunningConfigurationId': 'e09e58c3-15bf-42ad-8a5e-5b3273846dda', + 'modifiedAt': '2023-12-29T22:19:02Z', + 'scenarioId': 'foundation-models', + 'startTime': '2023-11-20T11:28:32Z', + 'status': 'RUNNING', + 'submissionTime': '2023-11-20T11:26:26Z', + 'targetStatus': 'RUNNING' + }, + { + 'configurationId': 'cc3a48e2-036b-411f-8dc9-18ee16944754', + 'configurationName': 'nvidia--llama-3.2-nv-embedqa-1b', + 'createdAt': '2025-12-22T15:44:35Z', + 'deploymentUrl': + 'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/deebf33e5ec3450c', + 'details': { + 'resources': { + 'backend_details': { + 'model': { + 'name': 'nvidia--llama-3.2-nv-embedqa-1b', + 'version': 'latest' + } + } + }, + 'scaling': { + 'backend_details': {} + } + }, + 'id': 'deebf33e5ec3450c', + 'lastOperation': 'CREATE', + 'latestRunningConfigurationId': 'cc3a48e2-036b-411f-8dc9-18ee16944754', + 'modifiedAt': '2025-12-22T15:48:35Z', + 'scenarioId': 'foundation-models', + 'startTime': '2025-12-22T15:45:35Z', + 'status': 'RUNNING', + 'submissionTime': '2025-12-22T15:46:26Z', + 'targetStatus': 'RUNNING' + }, + # Mock instruct + { + 'configurationId': '31987320-54ab-4469-a165-78748fef22b0', + 'configurationName': 'gpt-4-instruct-latest', + 'createdAt': '2023-11-20T11:24:09Z', + 'deploymentUrl': + 'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dac9dca90be5213b', + 'details': { + 'resources': { + 'backend_details': { + 'model': { + 'name': 'gpt-4-instruct', + 'version': 'latest' + } + } + }, + 'scaling': { + 'backend_details': {} + } + }, + 'id': 'dac9dca90be5213b', + 'lastOperation': 'CREATE', + 'latestRunningConfigurationId': '31987320-54ab-4469-a165-78748fef22b0', + 'modifiedAt': '2023-12-29T22:19:02Z', + 'scenarioId': 'foundation-models', + 'startTime': '2023-11-20T11:28:32Z', + 'status': 'RUNNING', + 'submissionTime': '2023-11-20T11:26:26Z', + 'targetStatus': 'RUNNING' + }, + { + "configurationId": "7785d039-b3cf-4250-969b-b5ac74047abc", + "configurationName": "gemini pro", + "createdAt": "2024-04-18T14:09:58Z", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d000a84bce0a333d", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "gemini-2.0-flash", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "d000a84bce0a333d", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "7785d039-b3cf-4250-969b-b5ac74047abc", + "modifiedAt": "2024-05-07T14:30:43Z", + "scenarioId": "foundation-models", + "startTime": "2024-04-18T14:15:21Z", + "status": "RUNNING", + "submissionTime": "2024-04-18T14:12:37Z", + "targetStatus": "RUNNING" + }, + # Mock amazon--bedrock + { + "configurationId": "2cfd83e3-e770-4469-b056-ecab0e0b4e10", + "configurationName": "amazon--nova-premier", + "createdAt": "2024-05-28T06:10:58Z", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/db0c5cf8ae2e09c9", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "amazon--nova-premier", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "db0c5cf8ae2e09c9", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "2cfd83e3-e770-4469-b056-ecab0e0b4e10", + "modifiedAt": "2024-06-03T08:08:34Z", + "scenarioId": "foundation-models", + "startTime": "2024-05-28T06:13:00Z", + "status": "RUNNING", + "submissionTime": "2024-05-28T06:11:26Z", + "targetStatus": "RUNNING" + }, + # Mock amazon--titan-embed-text + { + "configurationId": "f5d65f9e-fb55-400a-a1ea-501f32fd25db", + "configurationName": "amazon--titan-embed-text", + "createdAt": "2024-07-03T10:13:07Z", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/de1b7f924d70a828", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "amazon--titan-embed-text", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "de1b7f924d70a828", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "f5d65f9e-fb55-400a-a1ea-501f32fd25db", + "modifiedAt": "2024-07-05T06:49:32Z", + "scenarioId": "foundation-models", + "startTime": "2024-07-03T10:14:49Z", + "status": "RUNNING", + "submissionTime": "2024-07-03T10:13:37Z", + "targetStatus": "RUNNING" + }, + # Mock cohere--command-a-reasoning + { + "configurationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "configurationName": "cohere--command-a-reasoning", + "createdAt": "2024-08-15T09:30:00Z", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dc0a1b2c3d4e5f67", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "cohere--command-a-reasoning", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "dc0a1b2c3d4e5f67", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "modifiedAt": "2024-08-20T14:22:15Z", + "scenarioId": "foundation-models", + "startTime": "2024-08-15T09:35:42Z", + "status": "RUNNING", + "submissionTime": "2024-08-15T09:32:18Z", + "targetStatus": "RUNNING" + }, + ] +} + +# {{apiurl}}/v2/lm/deployments +GET_DEPLOYMENTS_RESPONSE_ORCHESTRATION = { + 'count': + 2, + 'resources': [ + { + "id": "d7f9c215310f5a11", + "createdAt": "2024-11-05T07:52:21Z", + "modifiedAt": "2025-01-14T15:51:06Z", + "status": "RUNNING", + "details": { + "resources": { + "backendDetails": {}, + "backend_details": {} + }, + "scaling": { + "backendDetails": {}, + "backend_details": {} + } + }, + "scenarioId": "orchestration", + "configurationId": "c802af4b-64f7-4e8a-955b-3dd49bdd7abb", + "latestRunningConfigurationId": "c802af4b-64f7-4e8a-955b-3dd49bdd7abb", + "lastOperation": "CREATE", + "targetStatus": "RUNNING", + "submissionTime": "2024-11-05T07:52:58Z", + "startTime": "2024-11-05T07:54:18Z", + "configurationName": "orchestration-config-1", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d7f9c215310f5a11" + }, + { + "id": "dea20c27f7fe0eca", + "createdAt": "2024-09-11T09:42:32Z", + "modifiedAt": "2025-01-14T15:51:05Z", + "status": "RUNNING", + "details": { + "resources": { + "backendDetails": {}, + "backend_details": {} + }, + "scaling": { + "backendDetails": {}, + "backend_details": {} + } + }, + "scenarioId": "orchestration", + "configurationId": "0152d9f0-694f-4bd2-a287-f7d270c9db60", + "latestRunningConfigurationId": "0152d9f0-694f-4bd2-a287-f7d270c9db60", + "lastOperation": "CREATE", + "targetStatus": "RUNNING", + "submissionTime": "2024-09-11T09:43:16Z", + "startTime": "2024-09-11T09:45:12Z", + "configurationName": "orchestration-config-2", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dea20c27f7fe0eca" + } + ] +} +# Mock custom scenario +GET_DEPLOYMENTS_RESPONSE_CUSTOM_SCENARIO = { + 'count': + 1, + 'resources': [ + { + "configurationId": "cdcf4374-ba3e-4aa3-9f32-86b90ac57506", + "configurationName": "dox-llm-cinderella-infer2.l-v0.2.7", + "createdAt": "2023-12-01T14:53:33Z", + "deploymentUrl": "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/df0758c763b18d6e", + "details": { + "resources": { + "backend_details": { + "predictor": { + "resource_plan": "infer2.l" + } + } + }, + "scaling": { + "backend_details": { + "predictor": { + "max_replicas": "1", + "min_replicas": "1", + "running_replicas": 1 + } + } + } + }, + "id": "df0758c763b18d6e", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "cdcf4374-ba3e-4aa3-9f32-86b90ac57506", + "modifiedAt": "2024-01-19T13:46:46Z", + "scenarioId": "dox-llm", + "startTime": "2023-12-27T08:45:28Z", + "status": "RUNNING", + "submissionTime": "2023-12-01T14:55:55Z", + "targetStatus": "RUNNING" + } + ] +} +# {{apiurl}}/v2/lm/configurations/.* +GET_CONFIGURATIONS_RESPONSE_GPT35 = { + 'createdAt': '2023-11-17T13:02:28Z', + 'executableId': 'azure-openai', + 'id': 'ad4a2c61-875a-416a-96b6-24ed1347a164', + 'inputArtifactBindings': [], + 'name': 'gpt-4o-mini', + 'parameterBindings': [{ + 'key': 'modelName', + 'value': 'gpt-4o-mini' + }, { + 'key': 'modelVersion', + 'value': 'latest' + }], + 'scenarioId': 'foundation-models' +} + +GET_CONFIGURATIONS_RESPONSE_EMB = { + 'createdAt': '2023-11-20T11:23:47Z', + 'executableId': 'azure-openai', + 'id': 'e09e58c3-15bf-42ad-8a5e-5b3273846dda', + 'inputArtifactBindings': [], + 'name': 'text-embedding-ada-002-latest', + 'parameterBindings': [{ + 'key': 'modelName', + 'value': 'text-embedding-ada-002' + }], + 'scenarioId': 'foundation-models' +} + +GET_CONFIGURATIONS_RESPONSE_GPT35_INSTRUCT = { + 'createdAt': '2023-11-20T11:23:47Z', + 'executableId': 'azure-openai', + 'id': '31987320-54ab-4469-a165-78748fef22b0', + 'inputArtifactBindings': [], + 'name': 'gpt-4-instruct-latest', + 'parameterBindings': [{ + 'key': 'modelName', + 'value': 'gpt-4-instruct' + }], + 'scenarioId': 'foundation-models' +} + +GET_CONFIGURATIONS_RESPONSE_GEMINI = { + "createdAt": "2024-05-07T14:35:13Z", + "executableId": "gcp-vertexai", + "id": "7785d039-b3cf-4250-969b-b5ac74047abc", + "inputArtifactBindings": [], + "name": "gemini pro", + "parameterBindings": [ + { + "key": "modelName", + "value": "gemini-2.0-flash" + }, + { + "key": "modelVersion", + "value": "latest" + } + ], + "scenarioId": "foundation-models" +} + +GET_CONFIGURATIONS_RESPONSE_AMAZON_TITAN_EMBED_TEXT = { + "createdAt": "2024-05-28T06:10:54Z", + "executableId": "aws-bedrock", + "id": "f5d65f9e-fb55-400a-a1ea-501f32fd25db", + "inputArtifactBindings": [], + "name": "amazon--titan-embed-text", + "parameterBindings": [ + {"key": "modelVersion", "value": "latest"}, + {"key": "modelName", "value": "amazon--titan-embed-text"}, + ], + "scenarioId": "foundation-models", +} + +GET_CONFIGURATIONS_RESPONSE_AMAZON_BEDROCK_NOVA = { + "createdAt": "2024-05-28T06:10:54Z", + "executableId": "aws-bedrock", + "id": "2cfd83e3-e770-4469-b056-ecab0e0b4e10", + "inputArtifactBindings": [], + "name": "amazon--nova-premier", + "parameterBindings": [ + {"key": "modelVersion", "value": "latest"}, + {"key": "modelName", "value": "amazon--nova-premier"}, + ], + "scenarioId": "foundation-models", +} + +GET_CONFIGURATIONS_RESPONSE_COHERE_COMMAND_A_REASONING = { + "createdAt": "2024-08-15T09:30:00Z", + "executableId": "cohere", + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "inputArtifactBindings": [], + "name": "cohere--command-a-reasoning", + "parameterBindings": [ + {"key": "modelVersion", "value": "latest"}, + {"key": "modelName", "value": "cohere--command-a-reasoning"}, + ], + "scenarioId": "foundation-models", +} + +GET_CONFIGURATIONS_RESPONSE_ORCHESTRATION_CONFIG_1 = { + "id": "c802af4b-64f7-4e8a-955b-3dd49bdd7abb", + "createdAt": "2024-11-05T07:42:52Z", + "name": "orchestration-config-1", + "executableId": "orchestration", + "scenarioId": "orchestration", + "parameterBindings": [], + "inputArtifactBindings": [] +} + +GET_CONFIGURATIONS_RESPONSE_ORCHESTRATION_CONFIG_2 = { + "id": "0152d9f0-694f-4bd2-a287-f7d270c9db60", + "createdAt": "2024-09-11T09:42:32Z", + "name": "orchestration-config-2", + "executableId": "orchestration", + "scenarioId": "orchestration", + "parameterBindings": [], + "inputArtifactBindings": [] +} + +GET_CONFIGURATIONS_RESPONSE_CINDERELLA_INSTRUCT = { + "createdAt": "2023-12-01T14:53:09Z", + "executableId": "dox-vllm-serve", + "id": "cdcf4374-ba3e-4aa3-9f32-86b90ac57506", + "inputArtifactBindings": [ + { + "artifactId": "33b8ab7f-3726-4a5c-8f32-2ff0cc744429", + "key": "textmodel" + } + ], + "name": "dox-llm-cinderella-infer2.l-v0.2.7", + "parameterBindings": [ + { + "key": "image", + "value": "dl-coe.common.repositories.cloud.sap/dox-vllm:0.0.2" + }, + { + "key": "resourcePlan", + "value": "infer2.l" + }, + { + "key": "minReplicas", + "value": "1" + }, + { + "key": "maxReplicas", + "value": "1" + }, + { + "key": "portNumber", + "value": "9000" + }, + { + "key": "gpu", + "value": "1" + }, + { + "key": "trustRemoteCode", + "value": "true" + }, + { + "key": "disableKernel", + "value": "False" + }, + { + "key": "huggingFaceOffline", + "value": "0" + }, + { + "key": "disableTelemetry", + "value": "1" + }, + { + "key": "revision", + "value": "main" + }, + { + "key": "additionalArgument", + "value": " " + }, + { + "key": "modelName", + "value": "cinderella/v2" + }, + { + "key": "tokenizer", + "value": "mistralai/Mistral-7B-v0.1" + } + ], + "scenarioId": "dox-llm" +} + +GET_CONFIGURATIONS_RESPONSE_NVIDIA_EMBED = { + "createdAt": "2024-12-01T10:00:00Z", + "executableId": "azure-openai", + "id": "cc3a48e2-036b-411f-8dc9-18ee16944754", + "inputArtifactBindings": [], + "name": "nvidia--llama-3.2-nv-embedqa-1b", + "parameterBindings": [ + { + "key": "modelName", + "value": "nvidia--llama-3.2-nv-embedqa-1b" + }, + { + "key": "modelVersion", + "value": "latest" + } + ], + "scenarioId": "foundation-models" +} + + +@contextmanager +def ai_core_ai_api_mocker(auth_url, base_url): + with requests_mock.Mocker() as mocker: + mocker.post(auth_url, json=GET_TOKEN_RESPONSE) + + def mock_get_deployments(request, context): + if 'scenarioid=foundation-models' in request.query: + return GET_DEPLOYMENTS_RESPONSE + elif 'scenarioid=dox-llm' in request.query: + return GET_DEPLOYMENTS_RESPONSE_CUSTOM_SCENARIO + elif 'orchestration' in request.query: + return GET_DEPLOYMENTS_RESPONSE_ORCHESTRATION + else: + raise ValueError('Unknown scenario') + + mocker.get(f'{base_url.rstrip("/")}/lm/deployments', json=mock_get_deployments) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/ad4a2c61-875a-416a-96b6-24ed1347a164', + json=GET_CONFIGURATIONS_RESPONSE_GPT35) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/e09e58c3-15bf-42ad-8a5e-5b3273846dda', + json=GET_CONFIGURATIONS_RESPONSE_EMB) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/31987320-54ab-4469-a165-78748fef22b0', + json=GET_CONFIGURATIONS_RESPONSE_GPT35_INSTRUCT) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/7785d039-b3cf-4250-969b-b5ac74047abc', + json=GET_CONFIGURATIONS_RESPONSE_GEMINI) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/2cfd83e3-e770-4469-b056-ecab0e0b4e10', + json=GET_CONFIGURATIONS_RESPONSE_AMAZON_BEDROCK_NOVA) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/f5d65f9e-fb55-400a-a1ea-501f32fd25db', + json=GET_CONFIGURATIONS_RESPONSE_AMAZON_TITAN_EMBED_TEXT) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/cdcf4374-ba3e-4aa3-9f32-86b90ac57506', + json=GET_CONFIGURATIONS_RESPONSE_CINDERELLA_INSTRUCT) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/c802af4b-64f7-4e8a-955b-3dd49bdd7abb', + json=GET_CONFIGURATIONS_RESPONSE_ORCHESTRATION_CONFIG_1) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/0152d9f0-694f-4bd2-a287-f7d270c9db60', + json=GET_CONFIGURATIONS_RESPONSE_ORCHESTRATION_CONFIG_2) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/cc3a48e2-036b-411f-8dc9-18ee16944754', + json=GET_CONFIGURATIONS_RESPONSE_NVIDIA_EMBED) + mocker.get(f'{base_url.rstrip("/")}/lm/configurations/a1b2c3d4-e5f6-7890-abcd-ef1234567890', + json=GET_CONFIGURATIONS_RESPONSE_COHERE_COMMAND_A_REASONING) + mocker.get(f'{base_url.rstrip("/")}/inference/deployments/d7f9c215310f5a11/completion', + json=GET_ORCHESTRATION_COMPLETION_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/inference/deployments/d7f9c215310f5a11/v2/completion', + json=GET_ORCHESTRATION_V2_COMPLETION_RESPONSE) + mocker.post(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches', + json=BATCH_CREATE_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches', + json=BATCH_LIST_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID}', + json=BATCH_DETAIL_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID_2}', + json=BATCH_DETAIL_RESPONSE) + mocker.delete(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID}', + json=BATCH_DELETE_RESPONSE) + mocker.delete(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID_2}', + json=BATCH_DELETE_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID}/status', + json=BATCH_STATUS_RESPONSE) + mocker.get(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID_2}/status', + json=BATCH_STATUS_RESPONSE) + mocker.patch(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID}/cancel', + json=BATCH_CANCEL_RESPONSE) + mocker.patch(f'{base_url.rstrip("/")}/llm-batch-service/v1/batches/{BATCH_ID_2}/cancel', + json=BATCH_CANCEL_RESPONSE) + yield + + +MOCK_BASE_URL = 'https://base_url/v2' +MOCK_AUTH_URL = 'https://auth_url/oauth/token' + +def get_mocked_ai_core_client(client_id='XXX'): + from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubProxyClient + from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context + + with proxy_version_context('gen-ai-hub'): + kwargs = dict( + client_id=client_id, + client_secret='YYY', + auth_url=MOCK_AUTH_URL, + base_url=MOCK_BASE_URL, + ) + proxy_client: GenAIHubProxyClient = get_proxy_client(**kwargs) + with ai_core_ai_api_mocker(auth_url=kwargs['auth_url'], base_url=kwargs['base_url']): + proxy_client.get_request_header() + proxy_client.get_deployments() + return proxy_client + + +GET_ORCHESTRATION_COMPLETION_RESPONSE = { + "request_id": "bf846179-66ef-4af5-8263-fee5028e69b2", + "module_results": { + "templating": [ + { + "role": "system", + "content": "This is a system message." + }, + { + "role": "user", + "content": "Hello, World!" + } + ], + "llm": { + "id": "", + "object": "chat.completion", + "created": 1738572663, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This confirms receipt of the message: \"Hello, World!\"\n" + }, + "finish_reason": "stop" + } + ], + "usage": { + "completion_tokens": 13, + "prompt_tokens": 10, + "total_tokens": 23 + } + } + }, + "orchestration_result": { + "id": "", + "object": "chat.completion", + "created": 1738572663, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This confirms receipt of the message: \"Hello, World!\"\n" + }, + "finish_reason": "stop" + } + ], + "usage": { + "completion_tokens": 13, + "prompt_tokens": 10, + "total_tokens": 23 + } + } +} + + +GET_ORCHESTRATION_V2_COMPLETION_RESPONSE = { + "request_id": "bf846179-66ef-4af5-8263-fee5028e69b2", + "intermediate_results": { + "templating": [ + { + "role": "system", + "content": "This is a system message." + }, + { + "role": "user", + "content": "Hello, World!" + } + ], + "llm": { + "id": "", + "object": "chat.completion", + "created": 1738572663, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This confirms receipt of the message: \"Hello, World!\"\n" + }, + "finish_reason": "stop" + } + ], + "usage": { + "completion_tokens": 13, + "prompt_tokens": 10, + "total_tokens": 23, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 3 + }, + "completion_tokens_details": { + "accepted_prediction_tokens": 3, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + } + }, + "final_result": { + "id": "", + "object": "chat.completion", + "created": 1738572663, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This confirms receipt of the message: \"Hello, World!\"\n" + }, + "finish_reason": "stop" + } + ], + "usage": { + "completion_tokens": 13, + "prompt_tokens": 10, + "total_tokens": 23, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 3 + }, + "completion_tokens_details": { + "accepted_prediction_tokens": 3, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + } + } + } +} + +@contextmanager +def orchestration_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_COMPLETION_RESPONSE)) + yield + +@contextmanager +def orchestration_completion_v2_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_COMPLETION_RESPONSE)) + yield + +GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE = { + "request_id": "emb-test-123", + "intermediate_results": None, + "final_result": { + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [0.005, -0.016, -0.016, 0.034, 0.010] + [0.0] * 3067, # 3072 dimensions + "index": 0 + } + ], + "model": "text-embedding-3-large", + "usage": { + "prompt_tokens": 2, + "total_tokens": 2 + } + } +} + +GET_ORCHESTRATION_V2_EMBEDDINGS_BATCH_RESPONSE = { + "request_id": "emb-batch-test-456", + "intermediate_results": None, + "final_result": { + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [0.1, 0.2, 0.3] + [0.0] * 253, # 256 dimensions + "index": 0 + }, + { + "object": "embedding", + "embedding": [0.4, 0.5, 0.6] + [0.0] * 253, + "index": 1 + }, + { + "object": "embedding", + "embedding": [0.7, 0.8, 0.9] + [0.0] * 253, + "index": 2 + } + ], + "model": "text-embedding-3-large", + "usage": { + "prompt_tokens": 10, + "total_tokens": 10 + } + } +} + +GET_ORCHESTRATION_V2_EMBEDDINGS_WITH_MASKING_RESPONSE = { + "request_id": "emb-masked-789", + "intermediate_results": { + "input_masking": { + "message": "Embedding input is masked successfully.", + "data": { + "masked_input": "Contact MASKED_PERSON at MASKED_EMAIL or call 555-123-4567." + } + } + }, + "final_result": { + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [0.01, 0.02, 0.03] + [0.0] * 3069, + "index": 0 + } + ], + "model": "text-embedding-3-large", + "usage": { + "prompt_tokens": 8, + "total_tokens": 8 + } + } +} + + +@contextmanager +def orchestration_embeddings_v2_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE)) + yield + + +@contextmanager +def orchestration_embeddings_v2_batch_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_BATCH_RESPONSE)) + yield + + +@contextmanager +def orchestration_embeddings_v2_with_masking_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=GET_ORCHESTRATION_V2_EMBEDDINGS_WITH_MASKING_RESPONSE)) + yield + + +@contextmanager +def orchestration_deployment_not_found_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(404, content=b'deployment not found')) + yield + +@contextmanager +def orchestration_too_many_requests_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(429, headers={"X-Custom-Header": "value"}, + json={"error": {"message": "too many requests"}})) + yield + + +def generate_events(): + # First event: templating event with system and user messages. + first_event = { + "request_id": "d1d50b3f-90f9-495c-b5ef-4bfeb175de02", + "module_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": None, + "templating": [ + {"content": "This is a system message.", "role": "system"}, + {"content": "Hello, World!", "role": "user"} + ], + "output_unmasking": None + }, + "orchestration_result": { + "id": "", + "object": "", + "created": 0, + "model": "", + "choices": [ + {"index": 0, "delta": {"content": "", "role": ""}, "finish_reason": "", "logprobs": None} + ], + "system_fingerprint": "" + } + } + # Yield the first event as a server-sent event (SSE) formatted string. + yield ("data: {}\n\n".format(json.dumps(first_event))).encode("utf-8") + + # List of tokens that will be returned one-by-one. + tokens = [ + "This", " con", "firm", "s re", "ceip", "t of", " the", + " sys", "tem ", "mess", "age:", ' "He', "llo,", " Wor", + "ld!", "\n" + ] + for token in tokens: + event = { + "request_id": "d1d50b3f-90f9-495c-b5ef-4bfeb175de02", + "module_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": { + "id": "", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": "stop", + "logprobs": None + } + ], + "system_fingerprint": None + }, + "templating": None, + "output_unmasking": None + }, + "orchestration_result": { + "id": "", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": "stop", + "logprobs": None + } + ], + "system_fingerprint": None + } + } + yield ("data: {}\n\n".format(json.dumps(event))).encode("utf-8") + +def generate_v2_events(): + # First event: templating event with system and user messages. + first_event = { + "request_id": "d1d50b3f-90f9-495c-b5ef-4bfeb175de02", + "intermediate_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": None, + "templating": [ + {"content": "This is a system message.", "role": "system"}, + {"content": "Hello, World!", "role": "user"} + ], + "output_unmasking": None + }, + "final_result": { + "id": "", + "object": "", + "created": 0, + "model": "", + "choices": [ + {"index": 0, "delta": {"content": "", "role": ""}, "finish_reason": "", "logprobs": None} + ], + "system_fingerprint": "" + } + } + # Yield the first event as a server-sent event (SSE) formatted string. + yield ("data: {}\n\n".format(json.dumps(first_event))).encode("utf-8") + + # List of tokens that will be returned one-by-one. + tokens = [ + "This", " con", "firm", "s re", "ceip", "t of", " the", + " sys", "tem ", "mess", "age:", ' "He', "llo,", " Wor", + "ld!", "\n" + ] + for token in tokens: + event = { + "request_id": "d1d50b3f-90f9-495c-b5ef-4bfeb175de02", + "intermediate_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": { + "id": "", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": "stop", + "logprobs": None + } + ], + "system_fingerprint": None + }, + "templating": None, + "output_unmasking": None + }, + "final_result": { + "id": "", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": "stop", + "logprobs": None + } + ], + "system_fingerprint": None + } + } + yield ("data: {}\n\n".format(json.dumps(event))).encode("utf-8") + +@contextmanager +def orchestration_stream_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock( + return_value=Response(200, stream=generate_events()) + ) + yield + +@contextmanager +def orchestration_stream_v2_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock( + return_value=Response(200, stream=generate_v2_events()) + ) + yield + +# Wrap the synchronous generator in an async generator. +async def async_generate_events(): + for event in generate_events(): + yield event + await asyncio.sleep(0) # yield control to the event loop + +# Wrap the synchronous generator in an async generator. +async def async_generate_v2_events(): + for event in generate_v2_events(): + yield event + await asyncio.sleep(0) # yield control to the event loop + +# A simple AsyncByteStream implementation that wraps an async iterator. +class AsyncIteratorStream(AsyncByteStream): + def __init__(self, aiter): + self.aiter = aiter + + async def __aiter__(self): + async for chunk in self.aiter: + yield chunk + + +@asynccontextmanager +async def orchestration_stream_completion_mocker_async(deployment_url): + with respx.mock: + respx.post(deployment_url).mock( + return_value=Response(200, stream=AsyncIteratorStream(async_generate_events())) + ) + yield + +@asynccontextmanager +async def orchestration_v2_stream_completion_mocker_async(deployment_url): + with respx.mock: + respx.post(deployment_url).mock( + return_value=Response(200, stream=AsyncIteratorStream(async_generate_v2_events())) + ) + yield + +OPENAI_CHAT_COMPLETION_RESPONSE = { + 'choices': [{ + 'finish_reason': 'stop', + 'index': 0, + 'message': { + 'content': 'Hello! How can I assist you today?', + 'role': 'assistant' + } + }], + 'created': + 1703886830, + 'id': + 'chatcmpl-8bF5a0q8oyDRXcVZucIBrV25AXT7H', + 'model': + 'gpt-4o-mini', + 'object': + 'chat.completion', + 'usage': { + 'completion_tokens': 9, + 'prompt_tokens': 19, + 'total_tokens': 28 + } +} + +# Cohere chat completion response with choices=None and data in model_extra +COHERE_CHAT_COMPLETION_RESPONSE = { + 'id': 'cohere-chat-completion-id', + 'object': 'chat.completion', + 'created': 1703886830, + 'model': 'cohere--command-a-reasoning', + 'choices': None, + 'finish_reason': 'COMPLETE', + 'message': { + 'content': [ + { + 'text': 'Hello! How can I assist you today?', + 'type': 'text' + } + ], + 'role': 'assistant' + } +} + + +@contextmanager +def openai_chat_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_CHAT_COMPLETION_RESPONSE)) + yield + + +@contextmanager +def cohere_chat_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=COHERE_CHAT_COMPLETION_RESPONSE)) + yield + + +john_doe = {"first_name": "John", "last_name": "Doe"} +OPENAI_STRUCTRED_OUTPUTS_RESPONSE = { + 'id': 'chatcmpl-C3kCRY6rwFZPhwYe9dCyFayJyxxlz', + 'choices': [{ + 'finish_reason': 'stop', + 'index': 0, + 'message': { + 'content': json.dumps(john_doe), + }, + }], + 'refusal': None, + 'role': 'human', + 'parsed': {'first_name': 'John', 'last_name': 'Doe'}, + 'created': 1755008611, + 'model': 'gpt-4o-mini', + 'object': 'chat.completion', + 'system_fingerprint': 'fp_efad92c60b' +} + + +@contextmanager +def openai_structured_outputs_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_STRUCTRED_OUTPUTS_RESPONSE)) + yield + + +random_floats = np.random.RandomState(1337).randn(1536) +normalized_floats = random_floats / np.sum(random_floats) + +OPENAI_EMBEDDINGS_RESPONSE = { + 'data': [{ + 'embedding': [*normalized_floats], + 'index': 0, + 'object': 'embedding' + }], + 'model': 'ada', + 'object': 'list', + 'usage': { + 'prompt_tokens': 5, + 'total_tokens': 5 + } +} + + +@contextmanager +def openai_embeddings_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_EMBEDDINGS_RESPONSE)) + yield + + +OPENAI_GPT35_INSTRUCT_RESPONSE = { + 'choices': [{ + 'finish_reason': + 'length', + 'index': + 0, + 'logprobs': + None, + 'text': + "\n\nSAP's primary business is enterprise software and services, including customer relationship management, supply chain management" + }], + 'created': + 1703951313, + 'id': + 'cmpl-8bVrdCCCWPxyCzZUbrCZ4utqTDi8A', + 'model': + 'gpt-4-instruct', + 'object': + 'text_completion', + 'usage': { + 'completion_tokens': 20, + 'prompt_tokens': 7, + 'total_tokens': 27 + } +} + + +@contextmanager +def openai_completion_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_GPT35_INSTRUCT_RESPONSE)) + yield + +RPT_RESPONSE_CODE_0 = { + "id": "c334f854-0d70-4c79-bd73-9ac581fd8cda", + "status": { + "code": 0, + "message": "ok" + }, + "predictions": [ + { + "COSTCENTER": [ + { + "prediction": "Office Furniture", + "confidence": 0.96 + } + ], + "ID": "35" + } + ], + "metadata": { + "num_columns": 5, + "num_rows": 2, + "num_predictions": 1, + "num_query_rows": 1 + } +} + +OPENAI_RESPONSES_RESPONSE = { + "id": "resp_0c53f34ef0cff6e20069d37bdfdf608195a0383bfbd1bcb96a", + "created_at": 1775467487.0, + "error": None, + "incomplete_details": None, + "instructions": None, + "metadata": {}, + "model": "gpt-5", + "object": "response", + "output": [ + { + "id": "rs_0c53f34ef0cff6e20069d37be069a48195b9daef0eede3cc92", + "summary": [], + "type": "reasoning", + "content": None, + "encrypted_content": None, + "status": None + }, + { + "id": "msg_0c53f34ef0cff6e20069d37be2ee3c8195bdbc665c51719627", + "content": [ + { + "annotations": [], + "text": "this is a test", + "type": "output_text", + "logprobs": [] + } + ], + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": True, + "temperature": 1.0, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "background": False, + "completed_at": 1775467491.0, + "conversation": None, + "max_output_tokens": None, + "max_tool_calls": None, + "previous_response_id": None, + "prompt": None, + "prompt_cache_key": None, + "prompt_cache_retention": None, + "reasoning": { + "effort": "medium", + "generate_summary": None, + "summary": None + }, + "safety_identifier": None, + "service_tier": "auto", + "status": "completed", + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "top_logprobs": 0, + "truncation": "disabled", + "usage": { + "input_tokens": 11, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 194, + "output_tokens_details": { + "reasoning_tokens": 128 + }, + "total_tokens": 205 + }, + "user": None, + "content_filters": [ + { + "blocked": False, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 1148, + "start_offset": 1130 + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": False, + "severity": "safe" + }, + "self_harm": { + "filtered": False, + "severity": "safe" + }, + "sexual": { + "filtered": False, + "severity": "safe" + }, + "violence": { + "filtered": False, + "severity": "safe" + } + }, + "source_type": "prompt" + }, + { + "blocked": False, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 782, + "start_offset": 0 + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": False, + "severity": "safe" + }, + "self_harm": { + "filtered": False, + "severity": "safe" + }, + "sexual": { + "filtered": False, + "severity": "safe" + }, + "violence": { + "filtered": False, + "severity": "safe" + } + }, + "source_type": "completion" + } + ], + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "store": True +} + + +@contextmanager +def openai_responses_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_RESPONSES_RESPONSE)) + yield + +OPENAI_RESPONSES_RESPONSE_PARSE = { + "id": "resp_08f37e5066d98b600069d3834afc948190979ba52db1e5713b", + "created_at": 1775469387.0, + "error": None, + "incomplete_details": None, + "instructions": None, + "metadata": {}, + "model": "gpt-5", + "object": "response", + "output": [ + { + "id": "rs_08f37e5066d98b600069d3834b94348190b29a3bd6ddd744f8", + "summary": [], + "type": "reasoning", + "content": None, + "encrypted_content": None, + "status": None, + }, + { + "id": "msg_08f37e5066d98b600069d38366ace88190b550ce4331f8c03f", + "content": [ + { + "annotations": [], + "text": '{"first_name":"John","last_name":"Doe"}', + "type": "output_text", + "logprobs": [], + "parsed": { + "first_name": "John", + "last_name": "Doe", + }, + } + ], + "role": "assistant", + "status": "completed", + "type": "message", + }, + ], + "parallel_tool_calls": True, + "temperature": 1.0, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "background": False, + "completed_at": 1775469415.0, + "conversation": None, + "max_output_tokens": None, + "max_tool_calls": None, + "previous_response_id": None, + "prompt": None, + "prompt_cache_key": None, + "prompt_cache_retention": None, + "reasoning": { + "effort": "medium", + "generate_summary": None, + "summary": None, + }, + "safety_identifier": None, + "service_tier": "auto", + "status": "completed", + "text": { + "format": { + "name": "Person", + "schema_": { + "additionalProperties": False, + "description": "A simple Pydantic model for testing structured outputs with OpenAI.", + "properties": { + "first_name": { + "title": "First Name", + "type": "string", + }, + "last_name": { + "title": "Last Name", + "type": "string", + }, + }, + "required": [ + "first_name", + "last_name", + ], + "title": "Person", + "type": "object", + }, + "type": "json_schema", + "description": None, + "strict": True, + }, + "verbosity": "medium", + }, + "top_logprobs": 0, + "truncation": "disabled", + "usage": { + "input_tokens": 75, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 1882, + "output_tokens_details": { + "reasoning_tokens": 1856, + }, + "total_tokens": 1957, + }, + "user": None, + "content_filters": [ + { + "blocked": False, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 1469, + "start_offset": 1438, + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": False, + "severity": "safe", + }, + "self_harm": { + "filtered": False, + "severity": "safe", + }, + "sexual": { + "filtered": False, + "severity": "safe", + }, + "violence": { + "filtered": False, + "severity": "safe", + }, + }, + "source_type": "prompt", + }, + { + "blocked": False, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 8613, + "start_offset": 0, + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": False, + "severity": "safe", + }, + "self_harm": { + "filtered": False, + "severity": "safe", + }, + "sexual": { + "filtered": False, + "severity": "safe", + }, + "violence": { + "filtered": False, + "severity": "safe", + }, + }, + "source_type": "completion", + }, + ], + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "store": True, +} + +@contextmanager +def openai_responses_structured_outputs_mocker(deployment_url): + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=OPENAI_RESPONSES_RESPONSE_PARSE)) + yield + +@contextmanager +def sap_rpt_moke_response_code_0(url: str): + with respx.mock: + respx.post(f"{url}/predict").mock(return_value=Response(200, json=RPT_RESPONSE_CODE_0)) + yield + +RPT_RESPONSE_CODE_2 = { + "status": { + "code": 2, + "message": "Invalid input" + }, + "detail": [ + { + "loc": [ + "prediction_config", + "target_columns", + 0, + "prediction_placeholder" + ], + "msg": "Field required", + "type": "missing" + } + ] +} + +@contextmanager +def sap_rpt_moke_response_code_2(url: str): + with respx.mock: + respx.post(f"{url}/predict").mock(return_value=Response(422, json=RPT_RESPONSE_CODE_2)) + yield + +@contextmanager +def openai_stream_completion_mocker(deployment_url): + def stream_events(*args): + for delta, finish_reason in ( + ({"role": "assistant", "content": ""}, None), + ({"content": "Hi"}, None), + ({"content": "!"}, None), + ({}, "stop"), + ): + yield "data: {}\n\n".format( + json.dumps( + { + "id": "chat-1", + "object": "chat.completion.chunk", + "created": 1695096940, + "model": "gpt-4o-mini", + "choices": [ + {"index": 0, "delta": delta, "finish_reason": finish_reason} + ], + } + ) + ) + + with respx.mock: + respx.post(deployment_url).mock(return_value=Response(200, json=list(stream_events()))) + yield + + +AMAZON_TITAN_EMBED_QUERY_RESPONSE = {"embedding": [0.82421875, 0.54296875, -0.63671875], "inputTextTokenCount": 14} + +AMAZON_BEDROCK_INVOKE_RESPONSE = { + "text": "Ahoy, fellow programmer! I'm curious, what kind of programming do you enjoy the most?" +} + +GOOGLE_GEMINI_GENERATE_CONTENT_RESPONSE = { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "This is a test response from the Gemini model." + } + ], + "role": "model" + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "candidatesTokenCount": 402, + "promptTokenCount": 8, + "totalTokenCount": 410 + } +} + +GOOGLE_GEMINI_INVOKE_RESPONSE = { + 'text': "Ahoy, fellow programmer! I'm curious, what kind of programming do you enjoy the most?"} + +GOOGLE_GEMINI_STREAM_GENERATE_CONTENT_RESPONSE = iter(['data: {"candidates": [{"content": {"role": "model","parts": [{' + '"text": "This is a mocked response from the Gemini model' + 'dust motes danced in"}]},"safetyRatings": [{"category": ' + '"HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE",' + '"probabilityScore": 0.03955078,"severity": ' + '"HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.02722168},' + '{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": ' + '"NEGLIGIBLE","probabilityScore": 0.07373047,"severity": ' + '"HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.032470703},' + '{"category": "HARM_CATEGORY_HARASSMENT","probability": ' + '"NEGLIGIBLE","probabilityScore": 0.13085938,"severity": ' + '"HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.026367188},' + '{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": ' + '"NEGLIGIBLE","probabilityScore": 0.14550781,"severity": ' + '"HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.125}]}]}\n\n']) + +GOOGLE_GEMINI_STREAM_ASYNC_RESPONSE = { + "candidates": [ + { + "content": { + "text": "This is a mocked response from the Gemini model." + }, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.03955078 + } + ] + } + ] +} + +AMAZON_BEDROCK_RESPONSE = [ + b'data: {"type":"message_start","message":{"id":"msg_bdrk_01Qgxvf5RHJD2PdoHeyHTV1x","type":"message",' + b'"role":"assistant","model":"claude-3-opus-20240229","content":[],"stop_reason":null,"stop_sequence":null,' + b'"usage":{"input_tokens":28,"output_tokens":1}}}\n\n', + b'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n', + b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here"}}\n\n', + b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" is a "}}\n\n', + b'data: {"type":"content_block_stop","index":0}\n\n', + b'data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},' + b'"usage":{"output_tokens":10}}\n\n', + b'data: {"type":"message_stop","amazon-bedrock-invocationMetrics":{"inputTokenCount":28,' + b'"outputTokenCount":10,"invocationLatency":976,"firstByteLatency":636}}\n\n'] + +AMAZON_BEDROCK_STREAM_GENERATE_CONTENT_RESPONSE = iter(AMAZON_BEDROCK_RESPONSE) + +AMAZON_BEDROCK_BROKEN_STREAM_RESPONSE = ( + { + 'ResponseMetadata': { + 'RequestId': 'example-request-id', + 'HTTPStatusCode': 200, + 'HTTPHeaders': { + 'content-type': 'application/vnd.amazon.eventstream', + } + }, + 'body': iter([ + {'chunk': + {'bytes': + b'{"outputText":"\\nOnce upon a time, there was a boat that was very old. It ' + b'had been used for many years to transport goods and people across the sea. ' + b'The boat was made of wood and had a st","index":0,' + b'"totalOutputTextTokenCount":null,"completionReason":null,"inputTextTokenCount":15}'}}, + {'chunk': + {'bytes': + b'{"outputText":"urdy frame. It had a large sail that could catch the wind and help ' + b'the boat move quickly across the water.\\n\\nOne day, the boat was being used to ' + b'transport a group of people to a new island. ' + b'The weather was bad, and the sea was rough. The boat was tossed around by the waves, ' + b'and the passengers wer","index":0,"totalOutputTextTokenCount":null,' + b'"completionReason":null,"inputTextTokenCount":null}'}}, + {'chunk': + # Broken byte indicator + {'bytes': """{"outputText":"e scared.\\n\\nThe captain of the boat was a skilled sailor,' + b' and he knew how to handle the boat in rough weather. He steered the boat carefully,' + b' and he used the sails to help him move forward.\\n\\nDespite the rough weather,' + b' the boat made it to the island safely. The passengers were grateful to t",' + b'"index":0,"totalOutputTextTokenCount":null,"completionReason":null,' + b'"inputTextTokenCount":null}'"""}}, + {'chunk': + {'bytes': b'{"outputText":"he captain for his skill and bravery.\\n\\nFrom that day on, ' + b'the boat became known as the \\"Courageous Boat.\\" It was a symbol of bravery ' + b'and resilience, and it was used to transport people and goods across the sea ' + b'for many years to come.","index":0,"totalOutputTextTokenCount":229,' + b'"completionReason":"FINISH","inputTextTokenCount":null,' + b'"amazon-bedrock-invocationMetrics":{"inputTokenCount":15,"outputTokenCount":229,' + b'"invocationLatency":5714,"firstByteLatency":2551}}'}} + ]) + }) + +# Constants for prompt registry testing +SCENARIO = 'test_scenario' +TEMPLATE_NAME = 'test_template' +TEMPLATE_ID = '123' +VERSION = '0.1.0' +TEMPLATE_GET_RESPONSE = PromptTemplateGetResponse( + id=TEMPLATE_ID, + name=TEMPLATE_NAME, + version=VERSION, + scenario=SCENARIO, + spec=PromptTemplateSpec( + template=[PromptTemplate(role='system', content='You are a system under test.')], + defaults={}, + additional_fields={} + ), + creation_timestamp=None, + managed_by=None, + is_version_head=None +) +TEMPLATE_LIST_RESPONSE = PromptTemplateListResponse( + count=1, + resources=[TEMPLATE_GET_RESPONSE] +) +TEMPLATE_POST_RESPONSE = PromptTemplatePostResponse( + message='Template created successfully', + id=TEMPLATE_ID, + scenario=SCENARIO, + name=TEMPLATE_NAME, + version=VERSION +) +TEMPLATE_DELETE_RESPONSE = PromptTemplateDeleteResponse(message='deleted') +TEMPLATE_SUBSTITUTION_REQUEST = PromptTemplateSubstitutionRequest(input_params={'inputExample': 'substitution test'}) +TEMPLATE_SUBSTITUTION_RESPONSE = PromptTemplateSubstitutionResponse( + parsed_prompt=[PromptTemplate(role='system', content='You are a system under test.')]) +TEMPLATE_YAML = """ +name: simple +version: 0.0.1 +scenario: my-scenario +spec: + template: + - role: "system" + content: "{{ ?instruction }}" + - role: "user" + content: "Some more {{ ?user_input }}" +""" + +ORCHESTRATION_CONFIG_NAME = 'test_config' +ORCHESTRATION_CONFIG_ID = '123' + +ORCHESTRATION_CONFIG_YAML = """ +name: simple +version: 0.0.1 +scenario: my-scenario +spec: + modules: + - prompt_templating: + prompt: + template: + - role: user + content: "First man on the moon, answer in json" + response_format: + type: json_object + model: + name: gpt-4o +""" + +ORCHESTRATION_CONFIG_POST_RESPONSE = { + "id": ORCHESTRATION_CONFIG_ID, + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "message": "Orchestration config created successfully." +} + +ORCHESTRATION_CONFIG_LIST_RESPONSE = { + "count": 3, + "resources": [ + { + "id": "", + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "creation_timestamp": "2024-08-18T14:50:17.157000", + "managed_by": "imperative", + "is_version_head": True + }, + { + "id": "", + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "creation_timestamp": "2024-08-19T10:30:45.123000", + "managed_by": "declarative", + "is_version_head": True + }, + { + "id": "", + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "creation_timestamp": "2024-08-19T10:30:45.123000", + "managed_by": "imperative", + "is_version_head": True + } + ] +} + +ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC = { + "count": 1, + "resources": [ + { + "id": "", + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "creation_timestamp": "2024-08-18T14:50:17.157000", + "managed_by": "imperative", + "is_version_head": True, + "spec": { + "modules": { + "prompt_templating": { + "prompt": { + "template_ref": { + "id": "" + } + }, + "model": { + "name": "", + "params": { + "temperature": 0.7, + "max_tokens": 500 + } + } + } + } + } + } + ] +} + +ORCHESTRATION_CONFIG_GET_RESPONSE = { + "id": ORCHESTRATION_CONFIG_ID, + "name": ORCHESTRATION_CONFIG_NAME, + "version": VERSION, + "scenario": SCENARIO, + "creation_timestamp": "string", + "managed_by": "string", + "is_version_head": True, + "resource_group_id": "string", + "spec": { + "modules": { + "prompt_templating": { + "prompt": { + "template": [ + { + "role": "user", + "content": "How can the features of AI in SAP BTP specifically {{'{{?groundingOutput}}'}}, be applied to {{'{{?inputContext}}'}}" + } + ], + "defaults": { + "inputContext": "The default text that will be used in the template if inputContext is not set" + } + }, + "model": { + "name": "gpt-4o-mini", + "version": "latest", + "params": { + "max_completion_tokens": 300, + "temperature": 0.1, + }, + "timeout": 600, + "max_retries": 2 + } + }, + "filtering": { + "output": { + "filters": [ + { + "type": "azure_content_safety", + "config": { + "hate": 0, + "self_harm": 0, + "sexual": 0, + "violence": 0, + "protected_material_code": False + } + }, + { + "type": "llama_guard_3_8b", + "config": { + "violent_crimes": True, + "non_violent_crimes": True, + "sex_crimes": True, + "child_exploitation": True, + "defamation": True, + "specialized_advice": True, + "privacy": True, + "intellectual_property": True, + "indiscriminate_weapons": True, + "hate": True, + "self_harm": True, + "sexual_content": True, + "elections": True, + "code_interpreter_abuse": True + } + } + ], + "stream_options": { + "overlap": 0 + } + } + }, + "masking": { + "providers": [ + { + "type": "sap_data_privacy_integration", + "method": "anonymization", + "entities": [ + { + "type": "profile-person", + "replacement_strategy": { + "method": "constant", + "value": "NAME_REDACTED" + } + }, + { + "regex": "string", + "replacement_strategy": { + "method": "constant", + "value": "NAME_REDACTED" + } + } + ], + "allowlist": [ + "SAP", + "Joule" + ], + "mask_grounding_input": { + "enabled": False + } + } + ] + }, + "grounding": { + "type": "document_grounding_service", + "config": { + "filters": [ + { + "id": "string", + "search_config": { + "max_chunk_count": 1 + }, + "data_repositories": [ + "*" + ], + "data_repository_type": "vector", + "data_repository_metadata": [ + { + "key": "string", + "value": [ + "string" + ] + } + ], + "document_metadata": [ + { + "key": "string", + "value": [ + "string" + ], + "select_mode": [ + "ignoreIfKeyAbsent" + ] + } + ], + "chunk_metadata": [ + { + "key": "string", + "value": [ + "string" + ] + } + ] + } + ], + "placeholders": { + "input": [ + "groundingInput" + ], + "output": "groundingOutput" + }, + "metadata_params": [ + "string" + ] + } + }, + "translation": { + "input": { + "type": "sap_document_translation", + "translate_messages_history": True, + "config": { + "source_language": "de-DE", + "apply_to": [ + { + "category": "placeholders", + "items": [ + "groundingInput", + "inputContext" + ], + "source_language": "de-DE" + } + ], + "target_language": "en-US" + } + }, + "output": { + "type": "sap_document_translation", + "config": { + "source_language": "de-DE", + "target_language": "en-US" + } + } + } + }, + "stream": { + "enabled": False + } + } +} + +ORCHESTRATION_CONFIG_DELETE_RESPONSE = { + "message": "Orchestration config deleted successfully." +} + + +# end of prompt registry testing constants + +class AsyncIteratorWrapper: + def __init__(self, async_generator): + self.async_generator = async_generator + + def __call__(self, *args, **kwargs): + return self + + def __aiter__(self): + return self.async_generator + + +# --------------------------------------------------------------------------- +# Batch service mock data and helpers +# --------------------------------------------------------------------------- + +BATCH_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +BATCH_ID_2 = "b2c3d4e5-f6a7-8901-bcde-f12345678901" +BATCHES_URL = f"{MOCK_BASE_URL}/llm-batch-service/v1/batches" + +BATCH_CREATE_RESPONSE = { + "id": BATCH_ID, + "created_at": "2026-04-30T10:00:00Z", + "status": "PENDING", + "message": "Batch job scheduled", +} + +BATCH_LIST_RESPONSE = { + "count": 2, + "resources": [ + { + "id": BATCH_ID, + "type": "llm-native", + "provider": "azure-openai", + "created_at": "2026-04-30T10:00:00Z", + "status": "COMPLETED", + }, + { + "id": BATCH_ID_2, + "type": "llm-native", + "provider": "azure-openai", + "created_at": "2026-04-30T11:00:00Z", + "status": "RUNNING", + }, + ], +} + +BATCH_DETAIL_RESPONSE = { + "id": BATCH_ID, + "type": "llm-native", + "provider": "azure-openai", + "created_at": "2026-04-30T10:00:00Z", + "input": {"uri": "ai://my-store/input/batch-input.jsonl"}, + "output": {"uri": "ai://my-store/output/"}, + "spec": {"model": "gpt-4.1-mini"}, + "status": { + "current_status": "COMPLETED", + "target_status": "COMPLETED", + "updated_at": "2026-04-30T12:00:00Z", + "message": None, + }, +} + +BATCH_STATUS_RESPONSE = { + "current_status": "RUNNING", + "target_status": "COMPLETED", + "updated_at": "2026-04-30T11:30:00Z", + "message": None, +} + +BATCH_CANCEL_RESPONSE = { + "id": BATCH_ID, + "created_at": "2026-04-30T10:00:00Z", + "message": "Batch job scheduled for cancellation", +} + +BATCH_DELETE_RESPONSE = { + "id": BATCH_ID, + "created_at": "2026-04-30T10:00:00Z", + "message": "Batch job deleted successfully", +} + +BATCH_ERROR_RESPONSE = { + "request_id": "d4a67ea1-2bf9-4df7-8105-d48203ccff76", + "message": "Batch job not found", +} + + +@contextmanager +def batch_create_mocker(): + with respx.mock: + respx.post(BATCHES_URL).mock(return_value=Response(202, json=BATCH_CREATE_RESPONSE)) + yield + + +@contextmanager +def batch_list_mocker(): + with respx.mock: + respx.get(BATCHES_URL).mock(return_value=Response(200, json=BATCH_LIST_RESPONSE)) + yield + + +@contextmanager +def batch_get_mocker(batch_id: str = BATCH_ID): + with respx.mock: + respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(200, json=BATCH_DETAIL_RESPONSE)) + yield + + +@contextmanager +def batch_status_mocker(batch_id: str = BATCH_ID): + with respx.mock: + respx.get(f"{BATCHES_URL}/{batch_id}/status").mock(return_value=Response(200, json=BATCH_STATUS_RESPONSE)) + yield + + +@contextmanager +def batch_cancel_mocker(batch_id: str = BATCH_ID): + with respx.mock: + respx.patch(f"{BATCHES_URL}/{batch_id}/cancel").mock(return_value=Response(202, json=BATCH_CANCEL_RESPONSE)) + yield + + +@contextmanager +def batch_delete_mocker(batch_id: str = BATCH_ID): + with respx.mock: + respx.delete(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(202, json=BATCH_DELETE_RESPONSE)) + yield + + +@contextmanager +def batch_not_found_mocker(batch_id: str = BATCH_ID): + with respx.mock: + respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(404, json=BATCH_ERROR_RESPONSE)) + yield + + +@contextmanager +def batch_create_error_mocker(): + with respx.mock: + respx.post(BATCHES_URL).mock(return_value=Response(400, json=BATCH_ERROR_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_create_mocker_async(): + with respx.mock: + respx.post(BATCHES_URL).mock(return_value=Response(202, json=BATCH_CREATE_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_list_mocker_async(): + with respx.mock: + respx.get(BATCHES_URL).mock(return_value=Response(200, json=BATCH_LIST_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_get_mocker_async(batch_id: str = BATCH_ID): + with respx.mock: + respx.get(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(200, json=BATCH_DETAIL_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_status_mocker_async(batch_id: str = BATCH_ID): + with respx.mock: + respx.get(f"{BATCHES_URL}/{batch_id}/status").mock(return_value=Response(200, json=BATCH_STATUS_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_cancel_mocker_async(batch_id: str = BATCH_ID): + with respx.mock: + respx.patch(f"{BATCHES_URL}/{batch_id}/cancel").mock(return_value=Response(202, json=BATCH_CANCEL_RESPONSE)) + yield + + +@asynccontextmanager +async def batch_delete_mocker_async(batch_id: str = BATCH_ID): + with respx.mock: + respx.delete(f"{BATCHES_URL}/{batch_id}").mock(return_value=Response(202, json=BATCH_DELETE_RESPONSE)) + yield diff --git a/packages/gen/tests/orchestration/__init__.py b/packages/gen/tests/orchestration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/orchestration/test_config.py b/packages/gen/tests/orchestration/test_config.py new file mode 100644 index 0000000..acf9864 --- /dev/null +++ b/packages/gen/tests/orchestration/test_config.py @@ -0,0 +1,54 @@ +import unittest + +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.content_filter import ContentFilter +from gen_ai_hub.orchestration.models.content_filtering import InputFiltering, OutputFiltering, ContentFiltering +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import Message, Role +from gen_ai_hub.orchestration.models.template import Template + + +class TestOrchestrationConfig(unittest.TestCase): + + def setUp(self): + self.template = Template( + messages=[Message(role=Role.USER, content="Hello, World!")] + ) + self.llm = LLM(name="gpt-4o-mini") + + def test_minimal_config(self): + config = OrchestrationConfig(template=self.template, llm=self.llm) + + json_data = config.to_dict() + self.assertEqual( + json_data["module_configurations"]["templating_module_config"], + self.template.to_dict(), + ) + self.assertEqual( + json_data["module_configurations"]["llm_module_config"], self.llm.to_dict() + ) + self.assertNotIn("filtering_module_config", json_data["module_configurations"]) + + def test_input_filtering(self): + input_filter = ContentFilter("new-content-filter", {"key": "value"}) + config = OrchestrationConfig( + template=self.template, llm=self.llm, + filtering=ContentFiltering(input_filtering=InputFiltering(filters=[input_filter])) + ) + json_data = config.to_dict() + self.assertEqual( + json_data["module_configurations"]["filtering_module_config"]["input"]["filters"][0], + input_filter.to_dict(), + ) + + def test_output_filtering(self): + output_filter = ContentFilter("new-content-filter", {"key": "value"}) + config = OrchestrationConfig( + template=self.template, llm=self.llm, + filtering=ContentFiltering(output_filtering=OutputFiltering(filters=[output_filter])) + ) + json_data = config.to_dict() + self.assertEqual( + json_data["module_configurations"]["filtering_module_config"]["output"]["filters"][0], + output_filter.to_dict(), + ) diff --git a/packages/gen/tests/orchestration/test_content_filter.py b/packages/gen/tests/orchestration/test_content_filter.py new file mode 100644 index 0000000..407ae00 --- /dev/null +++ b/packages/gen/tests/orchestration/test_content_filter.py @@ -0,0 +1,78 @@ +import unittest + +from gen_ai_hub.orchestration.models.azure_content_filter import AzureContentFilter, AzureThreshold +from gen_ai_hub.orchestration.models.content_filter import ContentFilter +from gen_ai_hub.orchestration.models.llama_guard_3_filter import LlamaGuard38bFilter + + +class TestContentFilter(unittest.TestCase): + + def test_content_filter_to_dict(self): + content_filter = ContentFilter("new-content-filter", {"key": "value"}) + expected_dict = {"type": "new-content-filter", "config": {"key": "value"}} + self.assertEqual(content_filter.to_dict(), expected_dict) + + +class TestAzureContentFilter(unittest.TestCase): + + def test_azure_content_filter_to_dict(self): + content_filter = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_SAFE_LOW_MEDIUM, + self_harm=AzureThreshold.ALLOW_SAFE_LOW) + + expected_dict = { + "type": 'azure_content_safety', + "config": { + "Hate": 0, + "Sexual": 6, + "Violence": 4, + "SelfHarm": 2, + }, + } + + self.assertEqual(content_filter.to_dict(), expected_dict) + + def test_azure_content_filter_with_invalid_threshold(self): + with self.assertRaises(ValueError): + AzureContentFilter(hate=10, sexual=6, violence=4, self_harm=2) + + def test_azure_content_filter_with_literal_thresholds(self): + content_filter = AzureContentFilter(hate=0, sexual=6, violence=4, self_harm=2) + + expected_dict = { + "type": 'azure_content_safety', + "config": { + "Hate": 0, + "Sexual": 6, + "Violence": 4, + "SelfHarm": 2, + }, + } + + self.assertEqual(content_filter.to_dict(), expected_dict) + + def test_llama_guard_content_filter_to_dict(self): + content_filter = LlamaGuard38bFilter() + + expected_dict = { + "type": 'llama_guard_3_8b', + "config":{ + "violent_crimes": False, + "non_violent_crimes": False, + "sex_crimes": False, + "child_exploitation": False, + "defamation": False, + "specialized_advice": False, + "privacy": False, + "intellectual_property": False, + "indiscriminate_weapons": False, + "hate": False, + "self_harm": False, + "sexual_content": False, + "elections": False, + "code_interpreter_abuse": False, + } + } + + self.assertEqual(content_filter.to_dict(), expected_dict) diff --git a/packages/gen/tests/orchestration/test_data_masking.py b/packages/gen/tests/orchestration/test_data_masking.py new file mode 100644 index 0000000..16323c3 --- /dev/null +++ b/packages/gen/tests/orchestration/test_data_masking.py @@ -0,0 +1,39 @@ +import unittest + +from gen_ai_hub.orchestration.models.data_masking import DataMasking +from gen_ai_hub.orchestration.models.sap_data_privacy_integration import SAPDataPrivacyIntegration, MaskingMethod, \ + ProfileEntity + + +class TestDataMasking(unittest.TestCase): + + def test_sap_data_privacy_integration(self): + data_masking = DataMasking( + providers=[ + SAPDataPrivacyIntegration( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[ProfileEntity.EMAIL], + allowlist=["SAP"] + ) + ] + ) + + expected_dict = { + "masking_providers": [ + { + "type": "sap_data_privacy_integration", + "method": "pseudonymization", + "entities": [ + { + "type": "profile-email" + } + ], + "allowlist": ["SAP"], + "mask_grounding_input": { + "enabled": False + } + } + ] + } + + self.assertEqual(data_masking.to_dict(), expected_dict) diff --git a/packages/gen/tests/orchestration/test_grounding.py b/packages/gen/tests/orchestration/test_grounding.py new file mode 100644 index 0000000..c024c81 --- /dev/null +++ b/packages/gen/tests/orchestration/test_grounding.py @@ -0,0 +1,59 @@ +import unittest + +from gen_ai_hub.orchestration.models.document_grounding import (GroundingType, DataRepositoryType, DocumentMetadata, + DocumentGroundingFilter, GroundingFilterSearch, + GroundingModule, DocumentGrounding) + + +class TestGrounding(unittest.TestCase): + + def test_document_metadata(self): + metadata = DocumentMetadata(key="key", value=["value"], select_mode=["ignoreIfKeyAbsent"]) + metadata_json = metadata.to_dict() + self.assertEqual(metadata_json["key"], "key") + self.assertEqual(metadata_json["value"], ["value"]) + self.assertEqual(metadata_json["select_mode"], ["ignoreIfKeyAbsent"]) + + def test_grounding_filter_search_configuration(self): + search_config = GroundingFilterSearch(max_chunk_count=10) + search_config_json = search_config.to_dict() + self.assertEqual(search_config_json["max_chunk_count"], 10) + self.assertIsNone(search_config_json.get("max_document_count")) + + def test_grounding_filter(self): + grounding_filter = DocumentGroundingFilter(id="id", + data_repository_type=DataRepositoryType.VECTOR.value, + data_repositories=["46b508c9-e490-4808-893b-b8e3361c4213"], + data_repository_metadata=[{ + "key": "data_repository_key", + "value": ["data_repository_value"], + }], + search_config=GroundingFilterSearch(max_chunk_count=3), + document_metadata=[DocumentMetadata( + key="keyTest", + value=["ValueTest1"], + select_mode=["ignoreIfKeyAbsent"] + )], + chunk_metadata=[{ + "key": "chunk_metadata_key", + "value": ["chunk_metadata_value"], + }] + ) + filter_json = grounding_filter.to_dict() + self.assertEqual(filter_json["id"], "id") + self.assertEqual(filter_json["data_repository_type"], "vector") + + def test_grounding_configuration(self): + filters = [DocumentGroundingFilter(id="id", data_repository_type=DataRepositoryType.VECTOR.value)] + grounding_config = GroundingModule( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE.value, + config=DocumentGrounding(input_params=["user_query"], output_param="grounding_response", + filters=filters, metadata_params=["metadata_param"]) + ) + config_json = grounding_config.to_dict() + self.assertEqual(config_json["type"], "document_grounding_service") + self.assertEqual(config_json["config"]["input_params"], ["user_query"]) + self.assertEqual(config_json["config"]["output_param"], "grounding_response") + self.assertEqual(config_json["config"]["filters"][0]["id"], "id") + self.assertEqual(config_json["config"]["filters"][0]["data_repository_type"], "vector") + self.assertEqual(config_json["config"]["metadata_params"], ["metadata_param"]) diff --git a/packages/gen/tests/orchestration/test_llm.py b/packages/gen/tests/orchestration/test_llm.py new file mode 100644 index 0000000..ad199c6 --- /dev/null +++ b/packages/gen/tests/orchestration/test_llm.py @@ -0,0 +1,31 @@ +import unittest + +from gen_ai_hub.orchestration.models.llm import LLM + + +class TestLLM(unittest.TestCase): + def test_llm_default_version(self): + llm = LLM("gpt-4o-mini") + json_data = llm.to_dict() + self.assertEqual(json_data["model_version"], "latest") + + def test_llm_with_no_parameters(self): + llm = LLM("gpt-4o-mini") + json_data = llm.to_dict() + self.assertEqual(json_data["model_params"], {}) + + def test_llm_custom_parameters(self): + params = {"temperature": 0.7, "max_tokens": 100} + llm = LLM("gpt-4o-mini", parameters=params) + json_data = llm.to_dict() + self.assertEqual(json_data["model_params"], params) + + def test_llm_json_serialization(self): + llm = LLM("gpt-4o-mini", "v1", {"temperature": 0.7}) + expected_dict = { + "model_name": "gpt-4o-mini", + "model_version": "v1", + "model_params": {"temperature": 0.7}, + } + json_data = llm.to_dict() + self.assertEqual(json_data, expected_dict) diff --git a/packages/gen/tests/orchestration/test_service.py b/packages/gen/tests/orchestration/test_service.py new file mode 100644 index 0000000..77fbb00 --- /dev/null +++ b/packages/gen/tests/orchestration/test_service.py @@ -0,0 +1,446 @@ + +import httpx +import unittest +from unittest.mock import Mock, patch, AsyncMock +from typing import cast + +from gen_ai_hub.orchestration.exceptions import OrchestrationError +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration.models.template import Template, TemplateValue +from gen_ai_hub.orchestration.service import OrchestrationService, cache_if_not_none +from tests.mock import ( + get_mocked_ai_core_client, + ai_core_ai_api_mocker, + orchestration_completion_mocker, + orchestration_stream_completion_mocker, + orchestration_stream_completion_mocker_async, + orchestration_deployment_not_found_mocker, + orchestration_too_many_requests_mocker, + GET_ORCHESTRATION_COMPLETION_RESPONSE +) + + +class TestOrchestrationService(unittest.TestCase): + + NOT_EXISTENT_DEPLOYMENT_ID = "not_existent" + + def setUp(self): + self.api_url = "https://api.example.com" + self.config = OrchestrationConfig( + llm=LLM(name="gemini-2.5-flash-lite"), + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + defaults=[TemplateValue("name", "World")], + ), + ) + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + + def test_caching(self): + + @cache_if_not_none + def func(arg): + func.calls += 1 + return arg + + func.calls = 0 + + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 1) + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 1) + func.cache_clear() + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 2) + self.assertEqual(func(None), None) + self.assertEqual(func.calls, 3) + self.assertEqual(func(None), None) + self.assertEqual(func.calls, 4) + + def test_initialization_with_empty_api_url(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(proxy_client=self.proxy_client) + self.assertEqual(client.api_url, "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d7f9c215310f5a11") + + def test_initialization_with_config_name(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(config_id="0152d9f0-694f-4bd2-a287-f7d270c9db60", proxy_client=self.proxy_client) + self.assertEqual(client.api_url, "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_config_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(config_name="orchestration-config-2", proxy_client=self.proxy_client) + self.assertEqual(client.api_url, "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id="dea20c27f7fe0eca", proxy_client=self.proxy_client) + self.assertEqual(client.api_url, "https://base_url/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_non_existing_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) + self.assertIn(self.NOT_EXISTENT_DEPLOYMENT_ID, client.api_url) + + def test_run_with_non_existing_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) + with orchestration_deployment_not_found_mocker(client.api_url + '/completion'): + with self.assertRaises(httpx.HTTPStatusError): + client.run(config=self.config) + + def test_run_without_config(self): + service = OrchestrationService(api_url=self.api_url, proxy_client=Mock()) + + with self.assertRaises(ValueError): + service.run() + + def test_run_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_completion_mocker(client.api_url + '/completion'): + client.run(config=self.config) + + def test_stream_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_stream_completion_mocker(client.api_url + '/completion'): + txt = '' + for chunk in client.stream(config=self.config): + if chunk.orchestration_result.choices: + txt += chunk.orchestration_result.choices[0].delta.content + self.assertEqual(txt, 'This confirms receipt of the system message: "Hello, World!\n') + + def test_too_many_requests(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_too_many_requests_mocker(client.api_url + '/completion'): + with self.assertRaises(OrchestrationError) as context: + client.run(config=self.config) + self.assertIn('X-Custom-Header', cast(OrchestrationError, context.exception).http_headers) + + def test_retry_backoff_with_retry_after_header(self): + """Test that retry backoff respects Retry-After header and applies jitter correctly.""" + import time + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + call_times = [] + retry_counts = [] + + # Patch the handle_retry method to track calls and timing + original_handle_retry = client.handle_retry + + def track_retry(*args, **kwargs): + call_times.append(time.time()) + retry_counts.append(args[0]) # retry_count is first arg + return original_handle_retry(*args, **kwargs) + + with orchestration_too_many_requests_mocker(client.api_url + '/completion'): + with patch.object(client, 'handle_retry', side_effect=track_retry): + with self.assertRaises(OrchestrationError) as context: + client.run_with_retries(config=self.config, max_retries=1, base_delay=1.0) + + error = cast(OrchestrationError, context.exception) + + # Verify retry was attempted (with max_retries=1, we get 1 retry attempt) + # The retry_count starts at 1 (after initial failure) + self.assertEqual(len(retry_counts), 1, "Expected 1 retry attempt") + self.assertEqual(retry_counts, [0], "Expected retry count of 0 for first retry") + + # Verify delay is positive + if len(call_times) >= 1: + self.assertGreater(len(call_times), 0, "Expected at least one retry delay measurement") + + # Verify error tracking + self.assertIn('X-Custom-Header', error.http_headers) + + def test_handle_retry_with_retry_after(self): + """Test that handle_retry uses exponential backoff with Retry-After header.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error without Retry-After header + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({"Retry-After": "3"}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Collect delays for multiple retries + delays = [] + for retry_count in range(3): + delay = client.handle_retry(retry_count=retry_count, base_delay=1.0, error=error, max_retries=5) + delays.append(delay) + + # Verify all delays are positive + for delay in delays: + self.assertGreater(delay, 0.0, "All delays should be positive") + self.assertLessEqual(delay, 60.0, "All delays should respect max_delay") + + def test_handle_retry_without_retry_after(self): + """Test that handle_retry uses exponential backoff when no Retry-After header.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error without Retry-After header + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Collect delays for multiple retries + delays = [] + for retry_count in range(3): + delay = client.handle_retry(retry_count=retry_count, base_delay=1.0, error=error, max_retries=5) + delays.append(delay) + + # Verify all delays are positive + for delay in delays: + self.assertGreater(delay, 0.0, "All delays should be positive") + self.assertLessEqual(delay, 60.0, "All delays should respect max_delay") + + def test_handle_retry_max_retries_exceeded(self): + """Test that handle_retry raises error when max_retries is exceeded.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Mock _should_retry to return True so we test the retry_count >= max_retries condition + with patch.object(client, '_should_retry', return_value=True): + # Test that it raises when retry_count >= max_retries + # Need to call handle_retry within an exception context since it uses bare 'raise' + try: + raise error + except httpx.HTTPStatusError as e: + with self.assertRaises(httpx.HTTPStatusError) as context: + client.handle_retry(retry_count=3, base_delay=1.0, error=e, max_retries=3) + + # Verify retries attribute was set + raised_error = context.exception + self.assertEqual(raised_error.retries, 3, "Expected retries attribute to be set") + + def test_calculate_backoff_with_min_delay(self): + """Test _calculate_backoff behavior with min_delay parameter.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Test with min_delay (simulating Retry-After header) + retry_after_delay = 5.0 + + # Case 1: When min_delay > exponential delay, returns capped (not min_delay) + # For retry_count=0, base_delay=1.0: exp = 1.0 * 2^0 = 1.0 + # With min_delay=5.0, max_delay=60.0: capped = min(1.0, 60.0) = 1.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) >= capped (1.0), returns capped = 1.0 + delay1 = client._calculate_backoff(retry_count=0, base_delay=1.0, min_delay=retry_after_delay, + max_delay=60.0) + self.assertEqual(delay1, 1.0, "When min_delay > exp_delay, should return capped exponential value") + + # Case 2: When exponential delay > min_delay, applies jitter between min_delay and capped + # For retry_count=3, base_delay=1.0: exp = 1.0 * 2^3 = 8.0 + # With min_delay=5.0, max_delay=60.0: capped = min(8.0, 60.0) = 8.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) < capped (8.0), returns random.uniform(5.0, 8.0) + delays = [ + client._calculate_backoff(retry_count=3, base_delay=1.0, min_delay=retry_after_delay, max_delay=60.0) + for _ in range(20)] + + # All delays should be between min_delay and the exponential cap + for delay in delays: + self.assertGreaterEqual(delay, retry_after_delay, "Delay should be at least min_delay") + self.assertLessEqual(delay, 8.0, "Delay should not exceed exponential cap for retry_count=3") + + # Verify jitter produces variance + unique_delays = len(set(delays)) + self.assertGreater(unique_delays, 1, "Expected jitter to produce varying delays") + + # Case 3: When min_delay == max_delay and exp > min_delay + # For retry_count=10, base_delay=1.0: exp = 1.0 * 2^10 = 1024.0 + # With min_delay=5.0, max_delay=5.0: capped = min(1024.0, 5.0) = 5.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) >= capped (5.0), returns capped = 5.0 + delay3 = client._calculate_backoff(retry_count=10, base_delay=1.0, min_delay=retry_after_delay, + max_delay=retry_after_delay) + self.assertEqual(delay3, retry_after_delay, "When min_delay == max_delay, should return that value") + + # Case 4: Test without min_delay (standard behavior) + # For retry_count=2, base_delay=1.0: exp = 1.0 * 2^2 = 4.0 + # With min_delay=0.0, max_delay=60.0: capped = min(4.0, 60.0) = 4.0 + # lower = max(0.0, 0.0) = 0.0 + # Returns random.uniform(0.0, 4.0) + delays_no_min = [ + client._calculate_backoff(retry_count=2, base_delay=1.0, min_delay=0.0, max_delay=60.0) + for _ in range(20)] + + for delay in delays_no_min: + self.assertGreaterEqual(delay, 0.0, "Delay should be non-negative") + self.assertLessEqual(delay, 4.0, "Delay should not exceed exponential value") + + # Verify variance with no min_delay + self.assertGreater(len(set(delays_no_min)), 1, "Expected jitter without min_delay") + + def test_calculate_backoff_exponential_progression(self): + """Test _calculate_backoff produces exponential backoff progression.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Collect average delays for different retry counts + num_samples = 50 + avg_delays = [] + + for retry_count in range(5): + delays = [client._calculate_backoff(retry_count=retry_count, base_delay=1.0, min_delay=0.0) + for _ in range(num_samples)] + avg_delays.append(sum(delays) / len(delays)) + + # Verify exponential growth in average delays + for i in range(len(avg_delays) - 1): + self.assertLess(avg_delays[i], avg_delays[i + 1], + f"Expected retry {i + 1} to have higher average delay than retry {i}") + + # Verify capping at max_delay + large_delays = [client._calculate_backoff(retry_count=10, base_delay=1.0, max_delay=60.0) + for _ in range(10)] + for delay in large_delays: + self.assertLessEqual(delay, 60.0, "Delay should be capped at max_delay") + + def test_calculate_backoff_custom_max_delay(self): + """Test _calculate_backoff respects custom max_delay.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + custom_max = 10.0 + delays = [client._calculate_backoff(retry_count=5, base_delay=1.0, max_delay=custom_max) + for _ in range(20)] + + for delay in delays: + self.assertLessEqual(delay, custom_max, "Delay should respect custom max_delay") + self.assertGreaterEqual(delay, 0.0, "Delay should be non-negative") + + def test_timeout_client_request(self): + class FakeResponse: + def raise_for_status(self): + pass # No-op for mock tests + + def json(self): + return GET_ORCHESTRATION_COMPLETION_RESPONSE + + + timeout_captured = {} # Capture the request kwargs from mocked post method + def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + # no timeout set in both httpx client and request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + + # timeout set in httpx client, not overwritten in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), 99.0) + + # timeout overwrite in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config, timeout=77.0) + self.assertEqual(timeout_captured.get("timeout"), 77.0) + + +class TestOrchestrationServiceAsync(unittest.IsolatedAsyncioTestCase): + + def setUp(self): + self.api_url = "https://api.example.com" + self.config = OrchestrationConfig( + llm=LLM(name="gemini-2.0-flash"), + template=Template( + messages=[ + SystemMessage("This is a system message."), + UserMessage("Hello, {{?name}}!"), + ], + defaults=[TemplateValue("name", "World")], + ), + ) + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + + async def test_async_run_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_completion_mocker(client.api_url + '/completion'): + await client.arun(config=self.config) + + async def test_async_timeout_client_request(self): + class FakeResponse: + def raise_for_status(self): + pass # No-op for mock tests + + def json(self): + return GET_ORCHESTRATION_COMPLETION_RESPONSE + + + timeout_captured = {} # Capture the request kwargs from mocked post method + async def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + # no timeout set in both httpx client and request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + + # timeout set in httpx client, not overwritten in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), 99.0) + + # timeout overwrite in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config, timeout=77.0) + self.assertEqual(timeout_captured.get("timeout"), 77.0) + + async def test_async_stream_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + async with orchestration_stream_completion_mocker_async(client.api_url + '/completion'): + txt = '' + async for chunk in await client.astream(config=self.config): + if chunk.orchestration_result.choices: + txt += chunk.orchestration_result.choices[0].delta.content + self.assertEqual(txt, 'This confirms receipt of the system message: "Hello, World!\n') + + async def test_async_run_with_retries(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_too_many_requests_mocker(client.api_url + '/completion'): + with self.assertRaises(OrchestrationError) as context: + await client.arun_with_retries(config=self.config, max_retries=1, base_delay=1.0) + self.assertIn('X-Custom-Header', cast(OrchestrationError, context.exception).http_headers) diff --git a/packages/gen/tests/orchestration/test_sse_client.py b/packages/gen/tests/orchestration/test_sse_client.py new file mode 100644 index 0000000..4bf8a51 --- /dev/null +++ b/packages/gen/tests/orchestration/test_sse_client.py @@ -0,0 +1,283 @@ +import json +import unittest +from unittest.mock import Mock + +from httpx import Response + +from gen_ai_hub.orchestration.sse_client import AsyncSSEClient + + +def create_valid_event(token="test"): + """ + Create a valid event structure matching the actual SSE response format. + + See tests/mock.py for structure details. + """ + return { + "request_id": "test-request-id", + "module_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": { + "id": "test-id", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": None, + "logprobs": None + } + ], + "system_fingerprint": None + }, + "templating": None, + "output_unmasking": None + }, + "orchestration_result": { + "id": "test-id", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": None, + "logprobs": None + } + ], + "system_fingerprint": None + } + } + + +class TestSSEClientBuffering(unittest.IsolatedAsyncioTestCase): + """Tests for AsyncSSEClient with manual buffering using aiter_text().""" + + def setUp(self): + """Set up test fixtures.""" + self.event_prefix = "data: " + self.final_message = "[DONE]" + + async def test_simple_complete_lines(self): + """Test that complete lines are processed correctly.""" + event1 = create_valid_event("test1") + event2 = create_valid_event("test2") + + chunks = [ + f"data: {json.dumps(event1)}\n", + f"data: {json.dumps(event2)}\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[1].request_id, "test-request-id") + + async def test_json_split_across_chunks(self): + """Test that JSON split across multiple chunks is handled correctly.""" + event = create_valid_event("split test") + event_str = json.dumps(event) + + # Split the JSON in the middle + mid = len(event_str) // 2 + part1 = event_str[:mid] + part2 = event_str[mid:] + + chunks = [ + f"data: {part1}", # First part without newline + f"{part2}\n", # Second part with newline + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].module_results.llm.choices[0].delta.content, "split test") + + async def test_multiple_events_in_single_chunk(self): + """Test that multiple events in a single chunk are processed correctly.""" + event1 = create_valid_event("first") + event2 = create_valid_event("second") + + chunks = [ + f"data: {json.dumps(event1)}\ndata: {json.dumps(event2)}\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0].module_results.llm.choices[0].delta.content, "first") + self.assertEqual(results[1].module_results.llm.choices[0].delta.content, "second") + + async def test_final_message_stops_iteration(self): + """Test that [DONE] message stops iteration.""" + event1 = create_valid_event("before done") + + chunks = [ + f"data: {json.dumps(event1)}\n", + f"data: {self.final_message}\n", + f"data: {json.dumps(event1)}\n", # This should not be processed + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + # Should only have one result before [DONE] + self.assertEqual(len(results), 1) + self.assertEqual(results[0].module_results.llm.choices[0].delta.content, "before done") + + async def test_empty_lines_ignored(self): + """Test that empty lines are ignored.""" + event = create_valid_event("test") + + chunks = [ + "\n", + f"data: {json.dumps(event)}\n", + "\n", + "\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_lines_without_event_prefix_ignored(self): + """Test that lines without the event prefix are ignored.""" + event = create_valid_event("test") + + chunks = [ + "invalid line\n", + f"data: {json.dumps(event)}\n", + "another invalid line\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_partial_line_at_end_of_stream(self): + """Test that a partial line at the end of the stream is processed.""" + event = create_valid_event("final") + + # Last chunk has no newline + chunks = [ + f"data: {json.dumps(event)}", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].module_results.llm.choices[0].delta.content, "final") + + async def test_very_small_chunks(self): + """Test handling of very small chunks (simulating slow network).""" + event = create_valid_event("test") + event_str = f"data: {json.dumps(event)}\n" + + # Split into very small chunks (2 characters each) + chunks = [event_str[i:i + 2] for i in range(0, len(event_str), 2)] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_complex_json_with_nested_objects(self): + """Test handling of complex nested JSON objects split across chunks.""" + event = create_valid_event("Hello") + event_str = json.dumps(event) + + # Split the JSON across multiple chunks + chunk_size = 20 + parts = [event_str[i:i + chunk_size] for i in range(0, len(event_str), chunk_size)] + + chunks = [f"data: {parts[0]}"] + for part in parts[1:-1]: + chunks.append(part) + chunks.append(f"{parts[-1]}\n") + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].module_results.llm.choices[0].delta.content, "Hello") + + async def _async_generator(self, items): + """Helper to create async generator from list.""" + for item in items: + yield item + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/orchestration/test_template.py b/packages/gen/tests/orchestration/test_template.py new file mode 100644 index 0000000..ad576bd --- /dev/null +++ b/packages/gen/tests/orchestration/test_template.py @@ -0,0 +1,275 @@ +import os +import base64 +import unittest +import tempfile + +from gen_ai_hub.orchestration.models.message import ( + Role, + SystemMessage, + UserMessage, + AssistantMessage, +) +from gen_ai_hub.orchestration.models.response_format import ( + ResponseFormatType, + ResponseFormatText, + ResponseFormatJsonObject, + ResponseFormatFactory, + ResponseFormatJsonSchema +) +from gen_ai_hub.orchestration.models.template import TemplateValue, Template +from gen_ai_hub.orchestration.models.multimodal_items import ImageItem, ImageDetailLevel + + +class TestTemplate(unittest.TestCase): + def test_template_with_defaults(self): + messages = [ + SystemMessage("You are a helpful assistant!"), + UserMessage("Hello, {{?name}}!"), + AssistantMessage("How can I help you today?"), + ] + defaults = [TemplateValue("name", "World")] + template = Template(messages, defaults) + + json_data = template.to_dict() + self.assertEqual(json_data["defaults"], {"name": "World"}) + self.assertEqual(len(json_data["template"]), len(messages)) + self.assertEqual(json_data["template"][1]["content"], "Hello, {{?name}}!") + + def test_template_without_defaults(self): + messages = [UserMessage("Simple message")] + template = Template(messages) + + json_data = template.to_dict() + self.assertEqual(json_data["defaults"], {}) + self.assertEqual(len(json_data["template"]), 1) + + def test_template_without_response_format(self): + messages = [UserMessage("Simple message")] + template = Template(messages) + + json_data = template.to_dict() + self.assertNotIn("response_format", json_data) + + def test_template_with_response_format_text(self): + messages = [UserMessage("Simple message")] + template = Template(messages, response_format='text') + + json_data = template.to_dict() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.TEXT) + + def test_template_with_response_format_json_object(self): + messages = [UserMessage("Simple message")] + template = Template(messages, response_format='json_object') + + json_data = template.to_dict() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.JSON_OBJECT) + + def test_template_with_response_format_json_schema(self): + messages = [UserMessage("Simple message")] + json_schema_example = { + "$id": "someid", + "$schema": "someshema", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + } + } + + response_format = ResponseFormatJsonSchema(name="test", schema=json_schema_example, strict=True) + template = Template(messages, response_format=response_format) + + json_data = template.to_dict() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.JSON_SCHEMA) + self.assertEqual(json_data["response_format"]["json_schema"]["name"], "test") + self.assertTrue(json_data["response_format"]["json_schema"]["strict"]) + self.assertEqual(json_data["response_format"]["json_schema"]["schema"], json_schema_example) + + def test_response_format_factory(self): + exp_result = ResponseFormatText() + response = ResponseFormatFactory.create_response_format_object(ResponseFormatType.TEXT) + self.assertEqual(response.to_dict(), exp_result.to_dict()) + + exp_result = ResponseFormatJsonObject() + response = ResponseFormatFactory.create_response_format_object(ResponseFormatType.JSON_OBJECT) + self.assertEqual(response.to_dict(), exp_result.to_dict()) + + exp_result = ResponseFormatJsonSchema(name="test", description="desc", schema={}, strict=True) + response = ResponseFormatFactory.create_response_format_object( + ResponseFormatJsonSchema(name="test", description="desc", schema={}, strict=True)) + self.assertEqual(response.to_dict(), exp_result.to_dict()) + + exp_result = None + response = ResponseFormatFactory.create_response_format_object(None) + self.assertEqual(response, exp_result) + + def test_response_format_name_not_valid(self): + not_valid_name = "test." + try: + ResponseFormatFactory.create_response_format_object( + ResponseFormatJsonSchema(name=not_valid_name, schema={})) + self.fail("Expected the validation to fail due to an invalid name format") + except ValueError: + pass + + def test_response_format_name_too_long(self): + name_too_long = "ThisIsAveryLongNameLongerThenExpectedItShouldBeMaximum64Characters" + try: + ResponseFormatFactory.create_response_format_object(ResponseFormatJsonSchema(name=name_too_long, schema={})) + self.fail("Expected the validation to fail due to length of name") + except ValueError: + pass + + +class TestTemplateWithTools(unittest.TestCase): + def test_template_with_tool_dict(self): + messages = [UserMessage("Say hello")] + tool_dict = { + "type": "function", + "function": { + "name": "hello", + "description": "Say hello", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False + }, + "strict": False + } + } + template = Template(messages, tools=[tool_dict]) + json_data = template.to_dict() + self.assertIn("tools", json_data) + self.assertEqual(json_data["tools"][0], tool_dict) + + def test_template_with_function_tool(self): + from gen_ai_hub.orchestration.models.tools import function_tool + + @function_tool() + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + messages = [UserMessage("Add two numbers")] + template = Template(messages, tools=[add]) + json_data = template.to_dict() + self.assertIn("tools", json_data) + tool = json_data["tools"][0] + self.assertEqual(tool["type"], "function") + self.assertEqual(tool["function"]["name"], "add") + self.assertEqual(tool["function"]["description"], "Add two numbers.") + self.assertIn("a", tool["function"]["parameters"]["properties"]) + self.assertIn("b", tool["function"]["parameters"]["properties"]) + + def test_template_with_multiple_tools(self): + from gen_ai_hub.orchestration.models.tools import function_tool + + @function_tool() + def foo(x: int) -> int: + """Foo.""" + return x + + @function_tool() + def bar(y: str) -> str: + """Bar.""" + return y + + messages = [UserMessage("Test multiple tools")] + template = Template(messages, tools=[foo, bar]) + json_data = template.to_dict() + self.assertIn("tools", json_data) + self.assertEqual(len(json_data["tools"]), 2) + self.assertEqual(json_data["tools"][0]["function"]["name"], "foo") + self.assertEqual(json_data["tools"][1]["function"]["name"], "bar") + + def test_template_with_plain_function_raises(self): + def plain_func(a: int) -> int: + return a + + messages = [UserMessage("Test plain function")] + template = Template(messages, tools=[plain_func]) + with self.assertRaises(ValueError) as cm: + template.to_dict() + self.assertIn("If you are passing a function, decorate it with @function_tool", str(cm.exception)) + +class TestUserMessageMultimodal(unittest.TestCase): + def test_user_message_with_single_string(self): + msg = UserMessage("Hello world!") + expected = { + "role": Role.USER, + "content": "Hello world!" + } + self.assertEqual(msg.to_dict(), expected) + + def test_user_message_with_list_of_strings(self): + msg = UserMessage(["Hello", "World"]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "World"} + ] + } + self.assertEqual(msg.to_dict(), expected) + + def test_user_message_with_string_and_image(self): + img = ImageItem(url="https://example.com/image.png") + msg = UserMessage(["Describe this image:", img]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "Describe this image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/image.png" + }} + ] + } + self.assertEqual(msg.to_dict(), expected) + + def test_user_message_with_multiple_images_and_text(self): + img1 = ImageItem(url="https://example.com/1.png", detail=ImageDetailLevel.LOW) + img2 = ImageItem(url="https://example.com/2.png", detail=ImageDetailLevel.HIGH) + msg = UserMessage(["First image:", img1, "Second image:", img2]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "First image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/1.png", + "detail": ImageDetailLevel.LOW + }}, + {"type": "text", "text": "Second image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/2.png", + "detail": ImageDetailLevel.HIGH + }} + ] + } + self.assertEqual(msg.to_dict(), expected) + + def test_image_item_from_file(self): + with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: + tmp.write(b"not a real image") + tmp_path = tmp.name + + try: + item = ImageItem.from_file(tmp_path) + self.assertTrue(item.url.startswith("data:image/png;base64,")) + # Check that the base64 part decodes to the original file content + encoded = item.url.split(",", 1)[1] + with open(tmp_path, "rb") as f: + original = f.read() + decoded = base64.b64decode(encoded) + + self.assertEqual(decoded, original) + finally: + os.unlink(tmp_path) + diff --git a/packages/gen/tests/orchestration/test_template_ref.py b/packages/gen/tests/orchestration/test_template_ref.py new file mode 100644 index 0000000..4ac8453 --- /dev/null +++ b/packages/gen/tests/orchestration/test_template_ref.py @@ -0,0 +1,35 @@ +import unittest + +from gen_ai_hub.orchestration.models.template_ref import TemplateRef + + +class TestTemplateRef(unittest.TestCase): + + def test_creates_instance_from_id(self): + template_ref = TemplateRef.from_id(prompt_template_id="test_template_id") + self.assertEqual(template_ref.id, "test_template_id") + self.assertEqual(template_ref.to_dict(), {"template_ref": {"id": "test_template_id"}}) + + def test_creates_instance_from_tuple(self): + template_ref = TemplateRef.from_tuple("test_scenario", "test_name", "test_version") + self.assertEqual(template_ref.scenario, "test_scenario") + self.assertEqual(template_ref.name, "test_name") + self.assertEqual(template_ref.version, "test_version") + self.assertEqual(template_ref.to_dict(), {"template_ref": + {"scenario": "test_scenario", + "name": "test_name", + "version": "test_version"} + } + ) + + def test_handles_kwargs(self): + template_ref = TemplateRef(id="test_template_id") + self.assertEqual(template_ref.to_dict(), {"template_ref": {"id": "test_template_id"}}) + + template_ref = TemplateRef(scenario="test_scenario", name="test_name", version="test_version") + self.assertEqual(template_ref.to_dict(), {"template_ref": + {"scenario": "test_scenario", + "name": "test_name", + "version": "test_version"} + } + ) diff --git a/packages/gen/tests/orchestration/test_tools.py b/packages/gen/tests/orchestration/test_tools.py new file mode 100644 index 0000000..8a1e9ee --- /dev/null +++ b/packages/gen/tests/orchestration/test_tools.py @@ -0,0 +1,124 @@ +import asyncio +import unittest +from typing import Optional + +from gen_ai_hub.orchestration.models.tools import FunctionTool, function_tool + + +class TestFunctionTool(unittest.TestCase): + def test_from_function_basic(self): + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tool = FunctionTool.from_function(add) + self.assertEqual(tool.name, "add") + self.assertEqual(tool.description, "Add two numbers.") + self.assertIn("a", tool.parameters["properties"]) + self.assertIn("b", tool.parameters["properties"]) + self.assertIn("a", tool.parameters["required"]) + self.assertIn("b", tool.parameters["required"]) + self.assertEqual(tool.parameters["properties"]["a"]["type"], "number") + self.assertEqual(tool.parameters["properties"]["b"]["type"], "number") + self.assertEqual(tool.execute(a=2, b=3), 5) + + def test_from_function_optional(self): + def greet(name: str, title: Optional[str] = None) -> str: + """Greet a person.""" + return f"Hello, {title + ' ' if title else ''}{name}" + + tool = FunctionTool.from_function(greet) + self.assertEqual(tool.name, "greet") + self.assertIn("title", tool.parameters["properties"]) + self.assertNotIn("title", tool.parameters["required"]) + self.assertTrue(tool.parameters["properties"]["title"]["nullable"]) + self.assertEqual(tool.execute(name="Alice"), "Hello, Alice") + self.assertEqual(tool.execute(name="Alice", title="Dr."), "Hello, Dr. Alice") + + def test_decorator(self): + @function_tool() + def echo(msg: str) -> str: + """Echo a message.""" + return msg + + self.assertIsInstance(echo, FunctionTool) + self.assertEqual(echo.name, "echo") + self.assertEqual(echo.execute(msg="hi"), "hi") + + def test_strict_mode(self): + def foo(x: int) -> int: + """Foo.""" + return x + + tool = FunctionTool.from_function(foo, strict=True) + with self.assertRaises(ValueError): + tool.execute(x=1, y=2) # y is not a valid parameter + + def test_missing_type_hint(self): + def no_type(a, b: int) -> int: + """No type for a.""" + return b + + with self.assertRaises(TypeError): + FunctionTool.from_function(no_type) + + def test_description_precedence(self): + # Case 1: No description provided, should use docstring + def sample(a: int) -> int: + """This is the docstring.""" + return a + + tool1 = FunctionTool.from_function(sample) + self.assertEqual(tool1.description, "This is the docstring.") + self.assertIn("description", tool1.to_dict()["function"]) + + # Case 2: Description provided, should take precedence over docstring + tool2 = FunctionTool.from_function(sample, description="Explicit description.") + self.assertEqual(tool2.description, "Explicit description.") + self.assertIn("description", tool2.to_dict()["function"]) + self.assertEqual(tool2.to_dict()["function"]["description"], "Explicit description.") + + # Case 3: No docstring and no description, description should not be in dict + @function_tool + def no_desc(a: int) -> int: + return a + + tool3 = no_desc + self.assertIsNone(tool3.description) + self.assertNotIn("description", tool3.to_dict()["function"]) + + +class TestFunctionToolAsync(unittest.IsolatedAsyncioTestCase): + async def test_async_function_tool(self): + async def async_add(a: int, b: int) -> int: + """Add two numbers asynchronously.""" + await asyncio.sleep(0.01) + return a + b + + tool = FunctionTool.from_function(async_add) + result = await tool.aexecute(a=2, b=3) + self.assertEqual(result, 5) + + async def test_async_decorator(self): + @function_tool() + async def async_echo(msg: str) -> str: + """Echo a message asynchronously.""" + await asyncio.sleep(0.01) + return msg + + self.assertIsInstance(async_echo, FunctionTool) + result = await async_echo.aexecute(msg="hi") + self.assertEqual(result, "hi") + + async def test_strict_mode_async(self): + async def foo(x: int) -> int: + """Async foo.""" + return x + + tool = FunctionTool.from_function(foo, strict=True) + result = await tool.aexecute(x=42) + self.assertEqual(result, 42) + + # This should raise ValueError because 'y' is not a valid parameter + with self.assertRaises(ValueError): + await tool.aexecute(x=1, y=2) diff --git a/packages/gen/tests/orchestration/test_translation.py b/packages/gen/tests/orchestration/test_translation.py new file mode 100644 index 0000000..4dd956f --- /dev/null +++ b/packages/gen/tests/orchestration/test_translation.py @@ -0,0 +1,101 @@ +import unittest + +from gen_ai_hub.orchestration.models.translation.translation import InputTranslationConfig, \ + InputTranslationModule, OutputTranslationConfig, OutputTranslationModule, TranslationType +from gen_ai_hub.orchestration.models.translation.sap_document_translation import SAPDocumentTranslation +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.message import Message, Role +from gen_ai_hub.orchestration.models.template import Template + + +class TestTranslation(unittest.TestCase): + def test_input_translation_config(self): + config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + config_dict = config.to_dict() + self.assertEqual(config_dict["source_language"], "en-US") + self.assertEqual(config_dict["target_language"], "de-DE") + + def test_input_translation_module(self): + config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + translation_module = InputTranslationModule(type=TranslationType.SAP_DOCUMENT_TRANSLATION, config=config) + module_dict = translation_module.to_dict() + self.assertEqual(module_dict["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(module_dict["config"]["source_language"], "en-US") + self.assertEqual(module_dict["config"]["target_language"], "de-DE") + + def test_output_translation_config(self): + config = OutputTranslationConfig(target_language="de-DE", source_language="en-US") + config_dict = config.to_dict() + self.assertEqual(config_dict["target_language"], "de-DE") + self.assertEqual(config_dict["source_language"], "en-US") + + def test_output_translation_module(self): + config = OutputTranslationConfig(target_language="de-DE", source_language="en-US") + translation_module = OutputTranslationModule(type=TranslationType.SAP_DOCUMENT_TRANSLATION, config=config) + module_dict = translation_module.to_dict() + self.assertEqual(module_dict["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(module_dict["config"]["target_language"], "de-DE") + self.assertEqual(module_dict["config"]["source_language"], "en-US") + + def test_sap_docu_translation(self): + input_config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + output_config = OutputTranslationConfig(target_language="de-DE", source_language="en-US") + + translation_module = SAPDocumentTranslation( + input_translation_config=input_config, + output_translation_config=output_config + ) + template = Template( + messages=[Message(role=Role.USER, content="Hello, World!")] + ) + llm = LLM(name="gpt-4o-mini") + + config = OrchestrationConfig( + template=template, llm=llm, + translation= translation_module + ) + + conf_dict = config.to_dict() + + self.assertIn("module_configurations", conf_dict) + self.assertIn("input_translation_module_config", conf_dict["module_configurations"]) + self.assertIn("output_translation_module_config", conf_dict["module_configurations"]) + + input_translation_module_config = translation_module.input_translation.to_dict() + output_translation_module_config = translation_module.input_translation.to_dict() + + self.assertEqual(input_translation_module_config["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(input_translation_module_config["config"]["source_language"], "en-US") + self.assertEqual(input_translation_module_config["config"]["target_language"], "de-DE") + + self.assertEqual(output_translation_module_config["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(output_translation_module_config["config"]["target_language"], "de-DE") + self.assertEqual(output_translation_module_config["config"]["source_language"], "en-US") + + def test_only_input_translation_module(self): + input_config = InputTranslationConfig(source_language="en-US", target_language="de-DE") + + translation_module = SAPDocumentTranslation( + input_translation_config=input_config) + + input_translation_module_config = translation_module.input_translation.to_dict() + + self.assertEqual(input_translation_module_config["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(input_translation_module_config["config"]["source_language"], "en-US") + self.assertEqual(input_translation_module_config["config"]["target_language"], "de-DE") + + self.assertIsNone(translation_module.output_translation, "Output translation module should be None.") + + def test_only_output_translation_module(self): + output_config = OutputTranslationConfig(target_language="de-DE", source_language="en-US") + + translation_module = SAPDocumentTranslation(output_translation_config=output_config) + + output_translation_module_config = translation_module.output_translation.to_dict() + + self.assertEqual(output_translation_module_config["type"], TranslationType.SAP_DOCUMENT_TRANSLATION) + self.assertEqual(output_translation_module_config["config"]["target_language"], "de-DE") + self.assertEqual(output_translation_module_config["config"]["source_language"], "en-US") + + self.assertIsNone(translation_module.input_translation, "Input translation module should be None.") \ No newline at end of file diff --git a/packages/gen/tests/orchestration_v2/__init__.py b/packages/gen/tests/orchestration_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/orchestration_v2/test_config_v2.py b/packages/gen/tests/orchestration_v2/test_config_v2.py new file mode 100644 index 0000000..c930839 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_config_v2.py @@ -0,0 +1,64 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.azure_content_filter import AzureContentSafetyInput, AzureContentSafetyOutput +from gen_ai_hub.orchestration_v2.models.content_filtering import FilteringModuleConfig, InputFiltering, OutputFiltering +from gen_ai_hub.orchestration_v2.models.content_filter import AzureContentSafetyInputFilterConfig, AzureContentSafetyOutputFilterConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig + + +class TestOrchestrationConfigV2(unittest.TestCase): + + def setUp(self): + self.template = Template( + template=[UserMessage(content="Hello, World!")] + ) + self.llm = LLMModelDetails(name="gpt-4o-mini") + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, + model=self.llm) + + def test_minimal_config(self): + module_config = ModuleConfig(prompt_templating=self.prompt_template) + config = OrchestrationConfig(modules=module_config) + + json_data = config.model_dump() + self.assertEqual( + json_data["modules"]["prompt_templating"]["prompt"], + self.template.model_dump(), + ) + self.assertEqual( + json_data["modules"]["prompt_templating"]["model"], self.llm.model_dump() + ) + self.assertNotIn("filtering_module_config", json_data["modules"]) + + def test_input_filtering(self): + input_filtering = InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=AzureContentSafetyInput(hate=0)) + ]) + content_filter_config = FilteringModuleConfig( + input= input_filtering + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + json_data = config.model_dump() + self.assertEqual( + json_data["modules"]["filtering"]["input"], + input_filtering.model_dump(), + ) + + def test_output_filtering(self): + output_filtering = OutputFiltering(filters=[ + AzureContentSafetyOutputFilterConfig(config=AzureContentSafetyOutput(hate=0)) + ]) + content_filter_config = FilteringModuleConfig( + output=output_filtering + ) + module_config = ModuleConfig(prompt_templating=self.prompt_template, filtering=content_filter_config) + config = OrchestrationConfig(modules=module_config) + json_data = config.model_dump() + self.assertEqual( + json_data["modules"]["filtering"]["output"], + output_filtering.model_dump(), + ) diff --git a/packages/gen/tests/orchestration_v2/test_content_filter_v2.py b/packages/gen/tests/orchestration_v2/test_content_filter_v2.py new file mode 100644 index 0000000..925277d --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_content_filter_v2.py @@ -0,0 +1,146 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.azure_content_filter import (AzureContentSafetyInput, AzureContentSafetyOutput, + AzureThreshold, AzureContentFilter) +from gen_ai_hub.orchestration_v2.models.content_filtering import InputFiltering +from gen_ai_hub.orchestration_v2.models.content_filter import (AzureContentSafetyInputFilterConfig, +AzureContentSafetyOutputFilterConfig, LlamaGuard38bFilterConfig, ContentFilter, ContentFilterProvider) +from gen_ai_hub.orchestration_v2.models.llama_guard_3_filter import LlamaGuard38bFilter + + +class TestContentFilters(unittest.TestCase): + + def test_azure_input_content_filter_to_dict(self): + input_filtering = InputFiltering(filters=[ + AzureContentSafetyInputFilterConfig(config=AzureContentSafetyInput( + hate=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_SAFE_LOW_MEDIUM, + self_harm=AzureThreshold.ALLOW_SAFE_LOW + )) + ]) + + expected_dict = { + "filters": [ + {"config": + { + "hate": 0, + "sexual": 6, + "violence": 4, + "self_harm": 2, + "prompt_shield": False + }, + "type": 'azure_content_safety' + }], + } + + self.assertEqual(input_filtering.model_dump(), expected_dict) + + def test_azure_input_content_filter_with_invalid_threshold(self): + with self.assertRaises(ValueError): + AzureContentSafetyInput(hate=10, sexual=6, violence=4, self_harm=2) + + def test_azure_input_content_filter_with_literal_thresholds(self): + content_filter_config = AzureContentSafetyInput(hate=0, sexual=6, violence=4, self_harm=2) + content_filter = AzureContentSafetyInputFilterConfig(config=content_filter_config) + + expected_dict = { + "type": 'azure_content_safety', + "config": { + "hate": 0, + "sexual": 6, + "violence": 4, + "self_harm": 2, + "prompt_shield": False + }, + } + + self.assertEqual(content_filter.model_dump(), expected_dict) + + def test_azure_output_content_filter_with_literal_thresholds(self): + content_filter_config = AzureContentSafetyOutput(hate=0, sexual=6, violence=4, self_harm=2) + content_filter = AzureContentSafetyOutputFilterConfig(config=content_filter_config) + + expected_dict = { + "type": 'azure_content_safety', + "config": { + "hate": 0, + "sexual": 6, + "violence": 4, + "self_harm": 2, + "protected_material_code": False + }, + } + + self.assertEqual(content_filter.model_dump(), expected_dict) + + def test_llama_guard_content_filter_to_dict(self): + content_filter = LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter()) + + expected_dict = { + "type": 'llama_guard_3_8b', + "config":{ + "violent_crimes": False, + "non_violent_crimes": False, + "sex_crimes": False, + "child_exploitation": False, + "defamation": False, + "specialized_advice": False, + "privacy": False, + "intellectual_property": False, + "indiscriminate_weapons": False, + "hate": False, + "self_harm": False, + "sexual_content": False, + "elections": False, + "code_interpreter_abuse": False, + } + } + + self.assertEqual(content_filter.model_dump(), expected_dict) + +class TestContentFiltersBackwardCompatibility(unittest.TestCase): + def test_azure_content_filter_to_dict_bc(self): + content_filter_config = AzureContentFilter(hate=AzureThreshold.ALLOW_SAFE, + sexual=AzureThreshold.ALLOW_ALL, + violence=AzureThreshold.ALLOW_SAFE_LOW_MEDIUM, + self_harm=AzureThreshold.ALLOW_SAFE_LOW) + content_filter = ContentFilter(type=ContentFilterProvider.AZURE, config=content_filter_config) + + expected_dict = { + "type": 'azure_content_safety', + "config": { + "hate": 0, + "sexual": 6, + "violence": 4, + "self_harm": 2, + }, + } + + self.assertEqual(content_filter.model_dump(), expected_dict) + + def test_llama_guard_content_filter_to_dict_backward_compatibility(self): + content_filter = ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, + config=LlamaGuard38bFilter()) + + expected_dict = { + "type": 'llama_guard_3_8b', + "config": { + "violent_crimes": False, + "non_violent_crimes": False, + "sex_crimes": False, + "child_exploitation": False, + "defamation": False, + "specialized_advice": False, + "privacy": False, + "intellectual_property": False, + "indiscriminate_weapons": False, + "hate": False, + "self_harm": False, + "sexual_content": False, + "elections": False, + "code_interpreter_abuse": False, + } + } + + self.assertEqual(content_filter.model_dump(), expected_dict) \ No newline at end of file diff --git a/packages/gen/tests/orchestration_v2/test_content_filtering_v2.py b/packages/gen/tests/orchestration_v2/test_content_filtering_v2.py new file mode 100644 index 0000000..3a44537 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_content_filtering_v2.py @@ -0,0 +1,48 @@ +import pytest +from pydantic import ValidationError +from gen_ai_hub.orchestration_v2.models.content_filtering import FilteringModuleConfig, InputFiltering, OutputFiltering +from gen_ai_hub.orchestration_v2.models.llama_guard_3_filter import LlamaGuard38bFilter +from gen_ai_hub.orchestration_v2.models.content_filter import (LlamaGuard38bFilterConfig, ContentFilter, + ContentFilterProvider) + + +def make_mock_content_filter(): + """Factory for minimal valid ContentFilter instance for tests.""" + return LlamaGuard38bFilterConfig(config=LlamaGuard38bFilter()) + +def make_mock_content_filter_backward_compatibility(): + """Factory for minimal valid ContentFilter instance for tests.""" + return ContentFilter(type=ContentFilterProvider.LLAMA_GUARD_3_8B, config=None) + +def test_filtering_module_config_min_properties_none(): + """Should raise ValidationError if both input and output are missing (enforced by model validator).""" + with pytest.raises(ValidationError): + FilteringModuleConfig() + +def test_filtering_module_config_min_properties_input_only(): + """Should succeed with only input filters set.""" + input_filters = InputFiltering(filters=[make_mock_content_filter()]) + config = FilteringModuleConfig(input=input_filters) + assert config.input is not None and config.output is None, "Output should be None when input is provided only" + +def test_filtering_module_config_min_properties_output_only(): + """Should succeed with only output filters set.""" + output_filters = OutputFiltering(filters=[make_mock_content_filter()]) + config = FilteringModuleConfig(output=output_filters) + assert config.output is not None and config.input is None, "Input should be None when output is provided only" + +def test_filtering_module_config_min_properties_both(): + """Should succeed when both input and output filters are set.""" + input_filters = InputFiltering(filters=[make_mock_content_filter()]) + output_filters = OutputFiltering(filters=[make_mock_content_filter()]) + config = FilteringModuleConfig(input=input_filters, output=output_filters) + assert config.input is not None, "Input should not be None" + assert config.output is not None, "Output should not be None" + +def test_filtering_module_config_min_properties_both_backward_compatibility(): + """Should succeed when both input and output filters are set.""" + input_filters = InputFiltering(filters=[make_mock_content_filter_backward_compatibility()]) + output_filters = OutputFiltering(filters=[make_mock_content_filter_backward_compatibility()]) + config = FilteringModuleConfig(input=input_filters, output=output_filters) + assert config.input is not None, "Input should not be None" + assert config.output is not None, "Output should not be None" diff --git a/packages/gen/tests/orchestration_v2/test_data_masking_v2.py b/packages/gen/tests/orchestration_v2/test_data_masking_v2.py new file mode 100644 index 0000000..83b3124 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_data_masking_v2.py @@ -0,0 +1,87 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.data_masking import (MaskingModuleConfig, MaskingProviderConfig, MaskingMethod, + DPIStandardEntity, ProfileEntity) + + + +class TestDataMasking(unittest.TestCase): + + def test_sap_data_privacy_integration_providers(self): + data_masking = MaskingModuleConfig( + providers=[MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ADDRESS), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + DPIStandardEntity(type=ProfileEntity.PERSON), ] + )] + ) + + expected_dict = { + 'providers': + [ + { + 'type': 'sap_data_privacy_integration', + 'method': 'anonymization', + 'entities': [ + {'type': 'profile-address'}, + {'type': 'profile-email'}, + {'type': 'profile-phone'}, + {'type': 'profile-person'} + ] + } + ] + } + + self.assertEqual(data_masking.model_dump(), expected_dict) + + def test_sap_data_privacy_integration_masking_providers(self): + data_masking = MaskingModuleConfig( + masking_providers=[MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ADDRESS), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + DPIStandardEntity(type=ProfileEntity.PERSON), ] + )] + ) + + expected_dict = { + 'masking_providers': + [ + { + 'type': 'sap_data_privacy_integration', + 'method': 'anonymization', + 'entities': [ + {'type': 'profile-address'}, + {'type': 'profile-email'}, + {'type': 'profile-phone'}, + {'type': 'profile-person'} + ] + } + ] + } + + self.assertEqual(data_masking.model_dump(), expected_dict) + + def test_maskin_module_config_error(self): + with self.assertRaises(ValueError): + MaskingModuleConfig() + + with self.assertRaises(ValueError): + MaskingModuleConfig( + masking_providers=[MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ADDRESS), + DPIStandardEntity(type=ProfileEntity.EMAIL), + DPIStandardEntity(type=ProfileEntity.PHONE), + DPIStandardEntity(type=ProfileEntity.PERSON), ] + )], + providers=[MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ADDRESS), + DPIStandardEntity(type=ProfileEntity.EMAIL), + ] + )] + ) diff --git a/packages/gen/tests/orchestration_v2/test_embeddings.py b/packages/gen/tests/orchestration_v2/test_embeddings.py new file mode 100644 index 0000000..62bc31e --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_embeddings.py @@ -0,0 +1,739 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.embeddings import ( + EmbeddingsEncodingFormat, + EmbeddingsInputType, + EmbeddingsModelParams, + EmbeddingsModelDetails, + EmbeddingsModelConfig, + EmbeddingsModuleConfigs, + EmbeddingsOrchestrationConfig, + EmbeddingsInput, + EmbeddingsUsage, + EmbeddingResult, + EmbeddingsResponse, + EmbeddingsPostResponse, + EmbeddingsRequest, +) +from gen_ai_hub.orchestration_v2.models.data_masking import ( + MaskingModuleConfig, + MaskingProviderConfig, + MaskingMethod, + DPIStandardEntity, + ProfileEntity, +) + + +class TestEmbeddingsEnums(unittest.TestCase): + """Tests for embedding-related enums.""" + + def test_encoding_format_values(self): + self.assertEqual(EmbeddingsEncodingFormat.FLOAT.value, "float") + self.assertEqual(EmbeddingsEncodingFormat.BASE64.value, "base64") + self.assertEqual(EmbeddingsEncodingFormat.BINARY.value, "binary") + + def test_input_type_values(self): + self.assertEqual(EmbeddingsInputType.TEXT.value, "text") + self.assertEqual(EmbeddingsInputType.DOCUMENT.value, "document") + self.assertEqual(EmbeddingsInputType.QUERY.value, "query") + + +class TestEmbeddingsModelParams(unittest.TestCase): + """Tests for EmbeddingsModelParams model.""" + + def test_empty_params(self): + params = EmbeddingsModelParams() + self.assertIsNone(params.dimensions) + self.assertIsNone(params.encoding_format) + self.assertIsNone(params.normalize) + self.assertEqual(params.model_dump(), {}) + + def test_params_with_dimensions(self): + params = EmbeddingsModelParams(dimensions=256) + self.assertEqual(params.dimensions, 256) + self.assertEqual(params.model_dump(), {"dimensions": 256}) + + def test_params_with_encoding_format(self): + params = EmbeddingsModelParams(encoding_format=EmbeddingsEncodingFormat.BASE64) + self.assertEqual(params.encoding_format, EmbeddingsEncodingFormat.BASE64) + self.assertEqual(params.model_dump(), {"encoding_format": "base64"}) + + def test_params_with_normalize(self): + params = EmbeddingsModelParams(normalize=True) + self.assertTrue(params.normalize) + self.assertEqual(params.model_dump(), {"normalize": True}) + + def test_full_params(self): + params = EmbeddingsModelParams( + dimensions=1536, + encoding_format=EmbeddingsEncodingFormat.FLOAT, + normalize=True + ) + expected = { + "dimensions": 1536, + "encoding_format": "float", + "normalize": True + } + self.assertEqual(params.model_dump(), expected) + + +class TestEmbeddingsModelDetails(unittest.TestCase): + """Tests for EmbeddingsModelDetails model.""" + + def test_minimal_model_details(self): + details = EmbeddingsModelDetails(name="text-embedding-3-large") + self.assertEqual(details.name, "text-embedding-3-large") + self.assertEqual(details.version, "latest") + self.assertIsNone(details.params) + self.assertEqual(details.timeout, 600) + self.assertEqual(details.max_retries, 2) + + def test_model_details_with_version(self): + details = EmbeddingsModelDetails(name="text-embedding-3-large", version="2024-01") + self.assertEqual(details.version, "2024-01") + + def test_model_details_with_params(self): + params = EmbeddingsModelParams(dimensions=512) + details = EmbeddingsModelDetails( + name="text-embedding-3-large", + params=params + ) + self.assertEqual(details.params.dimensions, 512) + + def test_model_details_with_custom_timeout(self): + details = EmbeddingsModelDetails(name="test-model", timeout=300) + self.assertEqual(details.timeout, 300) + + def test_model_details_with_custom_max_retries(self): + details = EmbeddingsModelDetails(name="test-model", max_retries=5) + self.assertEqual(details.max_retries, 5) + + def test_model_details_dump(self): + params = EmbeddingsModelParams(dimensions=256, normalize=True) + details = EmbeddingsModelDetails( + name="text-embedding-3-small", + version="latest", + params=params, + timeout=120, + max_retries=3 + ) + expected = { + "name": "text-embedding-3-small", + "version": "latest", + "params": { + "dimensions": 256, + "normalize": True + }, + "timeout": 120, + "max_retries": 3 + } + self.assertEqual(details.model_dump(), expected) + + +class TestEmbeddingsModelConfig(unittest.TestCase): + """Tests for EmbeddingsModelConfig model.""" + + def test_embeddings_model_config(self): + model_details = EmbeddingsModelDetails(name="text-embedding-3-large") + config = EmbeddingsModelConfig(model=model_details) + self.assertEqual(config.model.name, "text-embedding-3-large") + + def test_embeddings_model_config_dump(self): + model_details = EmbeddingsModelDetails(name="text-embedding-ada-002") + config = EmbeddingsModelConfig(model=model_details) + result = config.model_dump() + self.assertIn("model", result) + self.assertEqual(result["model"]["name"], "text-embedding-ada-002") + + +class TestEmbeddingsModuleConfigs(unittest.TestCase): + """Tests for EmbeddingsModuleConfigs model.""" + + def test_minimal_module_configs(self): + embeddings_config = EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + modules = EmbeddingsModuleConfigs(embeddings=embeddings_config) + self.assertIsNotNone(modules.embeddings) + self.assertIsNone(modules.masking) + + def test_module_configs_with_masking(self): + embeddings_config = EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + masking_config = MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.EMAIL)] + ) + ] + ) + modules = EmbeddingsModuleConfigs( + embeddings=embeddings_config, + masking=masking_config + ) + self.assertIsNotNone(modules.embeddings) + self.assertIsNotNone(modules.masking) + + def test_module_configs_dump(self): + embeddings_config = EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + modules = EmbeddingsModuleConfigs(embeddings=embeddings_config) + result = modules.model_dump() + self.assertIn("embeddings", result) + self.assertEqual(result["embeddings"]["model"]["name"], "text-embedding-3-large") + + +class TestEmbeddingsOrchestrationConfig(unittest.TestCase): + """Tests for EmbeddingsOrchestrationConfig model.""" + + def test_orchestration_config(self): + modules = EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + ) + config = EmbeddingsOrchestrationConfig(modules=modules) + self.assertEqual(config.modules.embeddings.model.name, "text-embedding-3-large") + + def test_orchestration_config_dump(self): + modules = EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-large", + params=EmbeddingsModelParams(dimensions=256) + ) + ) + ) + config = EmbeddingsOrchestrationConfig(modules=modules) + result = config.model_dump() + self.assertIn("modules", result) + self.assertEqual(result["modules"]["embeddings"]["model"]["params"]["dimensions"], 256) + + +class TestEmbeddingsInput(unittest.TestCase): + """Tests for EmbeddingsInput model.""" + + def test_single_text_input(self): + input_obj = EmbeddingsInput(text="Hello, World!") + self.assertEqual(input_obj.text, "Hello, World!") + self.assertIsNone(input_obj.type_) + + def test_list_text_input(self): + texts = ["Hello", "World", "Test"] + input_obj = EmbeddingsInput(text=texts) + self.assertEqual(input_obj.text, texts) + self.assertEqual(len(input_obj.text), 3) + + def test_input_with_type_text(self): + input_obj = EmbeddingsInput(text="Test", type=EmbeddingsInputType.TEXT) + self.assertEqual(input_obj.type_, EmbeddingsInputType.TEXT) + + def test_input_with_type_document(self): + input_obj = EmbeddingsInput(text="Document content", type=EmbeddingsInputType.DOCUMENT) + self.assertEqual(input_obj.type_, EmbeddingsInputType.DOCUMENT) + + def test_input_with_type_query(self): + input_obj = EmbeddingsInput(text="Search query?", type=EmbeddingsInputType.QUERY) + self.assertEqual(input_obj.type_, EmbeddingsInputType.QUERY) + + def test_input_dump_single_text(self): + input_obj = EmbeddingsInput(text="Hello") + result = input_obj.model_dump() + self.assertEqual(result, {"text": "Hello"}) + + def test_input_dump_with_type(self): + input_obj = EmbeddingsInput(text="Query", type=EmbeddingsInputType.QUERY) + result = input_obj.model_dump() + self.assertEqual(result, {"text": "Query", "type": "query"}) + + def test_input_dump_list(self): + input_obj = EmbeddingsInput(text=["a", "b", "c"]) + result = input_obj.model_dump() + self.assertEqual(result, {"text": ["a", "b", "c"]}) + + +class TestEmbeddingsUsage(unittest.TestCase): + """Tests for EmbeddingsUsage model.""" + + def test_usage(self): + usage = EmbeddingsUsage(prompt_tokens=10, total_tokens=10) + self.assertEqual(usage.prompt_tokens, 10) + self.assertEqual(usage.total_tokens, 10) + + def test_usage_dump(self): + usage = EmbeddingsUsage(prompt_tokens=100, total_tokens=100) + result = usage.model_dump() + self.assertEqual(result, {"prompt_tokens": 100, "total_tokens": 100}) + + +class TestEmbeddingResult(unittest.TestCase): + """Tests for EmbeddingResult model.""" + + def test_embedding_result_with_float_list(self): + embedding = [0.1, 0.2, 0.3, 0.4, 0.5] + result = EmbeddingResult(object="embedding", embedding=embedding, index=0) + self.assertEqual(result.object, "embedding") + self.assertEqual(result.embedding, embedding) + self.assertEqual(result.index, 0) + + def test_embedding_result_with_base64_string(self): + base64_embedding = "SGVsbG8gV29ybGQ=" + result = EmbeddingResult(object="embedding", embedding=base64_embedding, index=1) + self.assertEqual(result.embedding, base64_embedding) + self.assertEqual(result.index, 1) + + def test_embedding_result_dump(self): + result = EmbeddingResult( + object="embedding", + embedding=[0.1, 0.2, 0.3], + index=0 + ) + dumped = result.model_dump() + self.assertEqual(dumped["object"], "embedding") + self.assertEqual(dumped["embedding"], [0.1, 0.2, 0.3]) + self.assertEqual(dumped["index"], 0) + + +class TestEmbeddingsResponse(unittest.TestCase): + """Tests for EmbeddingsResponse model.""" + + def test_embeddings_response(self): + data = [ + EmbeddingResult(object="embedding", embedding=[0.1, 0.2], index=0), + EmbeddingResult(object="embedding", embedding=[0.3, 0.4], index=1), + ] + usage = EmbeddingsUsage(prompt_tokens=10, total_tokens=10) + response = EmbeddingsResponse( + object="list", + data=data, + model="text-embedding-3-large", + usage=usage + ) + self.assertEqual(response.object, "list") + self.assertEqual(len(response.data), 2) + self.assertEqual(response.model, "text-embedding-3-large") + self.assertEqual(response.usage.prompt_tokens, 10) + + def test_embeddings_response_dump(self): + data = [EmbeddingResult(object="embedding", embedding=[0.1], index=0)] + usage = EmbeddingsUsage(prompt_tokens=5, total_tokens=5) + response = EmbeddingsResponse( + object="list", + data=data, + model="test-model", + usage=usage + ) + result = response.model_dump() + self.assertEqual(result["object"], "list") + self.assertEqual(result["model"], "test-model") + self.assertIn("data", result) + self.assertIn("usage", result) + + +class TestEmbeddingsPostResponse(unittest.TestCase): + """Tests for EmbeddingsPostResponse model.""" + + def test_post_response_minimal(self): + final_result = EmbeddingsResponse( + object="list", + data=[EmbeddingResult(object="embedding", embedding=[0.1, 0.2], index=0)], + model="text-embedding-3-large", + usage=EmbeddingsUsage(prompt_tokens=5, total_tokens=5) + ) + response = EmbeddingsPostResponse( + request_id="test-123", + final_result=final_result + ) + self.assertEqual(response.request_id, "test-123") + self.assertIsNone(response.intermediate_results) + self.assertEqual(response.final_result.model, "text-embedding-3-large") + + def test_post_response_with_intermediate_results(self): + final_result = EmbeddingsResponse( + object="list", + data=[EmbeddingResult(object="embedding", embedding=[0.1], index=0)], + model="text-embedding-3-large", + usage=EmbeddingsUsage(prompt_tokens=5, total_tokens=5) + ) + intermediate = { + "input_masking": { + "message": "Embedding input is masked successfully.", + "data": {"masked_input": "Contact MASKED_PERSON at MASKED_EMAIL"} + } + } + response = EmbeddingsPostResponse( + request_id="test-456", + intermediate_results=intermediate, + final_result=final_result + ) + self.assertEqual(response.request_id, "test-456") + self.assertIsNotNone(response.intermediate_results) + self.assertIn("input_masking", response.intermediate_results) + + def test_post_response_dump(self): + final_result = EmbeddingsResponse( + object="list", + data=[EmbeddingResult(object="embedding", embedding=[0.5], index=0)], + model="test-model", + usage=EmbeddingsUsage(prompt_tokens=1, total_tokens=1) + ) + response = EmbeddingsPostResponse( + request_id="dump-test", + final_result=final_result + ) + result = response.model_dump() + self.assertEqual(result["request_id"], "dump-test") + self.assertIn("final_result", result) + + +class TestEmbeddingsRequest(unittest.TestCase): + """Tests for EmbeddingsRequest model.""" + + def test_embeddings_request(self): + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + ) + ) + input_obj = EmbeddingsInput(text="Hello World") + request = EmbeddingsRequest(config=config, input=input_obj) + self.assertEqual(request.config.modules.embeddings.model.name, "text-embedding-3-large") + self.assertEqual(request.input.text, "Hello World") + + def test_embeddings_request_dump(self): + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-large", + params=EmbeddingsModelParams(dimensions=256) + ) + ) + ) + ) + input_obj = EmbeddingsInput(text=["text1", "text2"], type=EmbeddingsInputType.DOCUMENT) + request = EmbeddingsRequest(config=config, input=input_obj) + result = request.model_dump() + + self.assertIn("config", result) + self.assertIn("input", result) + self.assertEqual(result["config"]["modules"]["embeddings"]["model"]["name"], "text-embedding-3-large") + self.assertEqual(result["config"]["modules"]["embeddings"]["model"]["params"]["dimensions"], 256) + self.assertEqual(result["input"]["text"], ["text1", "text2"]) + self.assertEqual(result["input"]["type"], "document") + + +class TestEmbeddingsServiceSync(unittest.TestCase): + """Tests for OrchestrationService embed method (sync).""" + + def setUp(self): + from tests.mock import get_mocked_ai_core_client, ai_core_ai_api_mocker + self.api_url = "https://api.example.com" + self.proxy_client = get_mocked_ai_core_client(client_id='testembeddingsclient') + self.ai_core_mocker = ai_core_ai_api_mocker + + self.embeddings_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + ) + ) + self.embeddings_input = EmbeddingsInput(text="Hello World!") + + def test_embed_single_text(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_mocker + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_mocker(service.api_url + '/v2/embeddings'): + response = service.embed(config=self.embeddings_config, input=self.embeddings_input) + + self.assertIsInstance(response, EmbeddingsPostResponse) + self.assertEqual(response.request_id, "emb-test-123") + self.assertEqual(response.final_result.model, "text-embedding-3-large") + self.assertEqual(len(response.final_result.data), 1) + self.assertEqual(response.final_result.data[0].index, 0) + self.assertEqual(len(response.final_result.data[0].embedding), 3072) + + def test_embed_batch_texts(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_batch_mocker + + batch_input = EmbeddingsInput(text=["Text 1", "Text 2", "Text 3"]) + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_batch_mocker(service.api_url + '/v2/embeddings'): + response = service.embed(config=self.embeddings_config, input=batch_input) + + self.assertEqual(len(response.final_result.data), 3) + for i, result in enumerate(response.final_result.data): + self.assertEqual(result.index, i) + self.assertEqual(len(result.embedding), 256) + + def test_embed_with_masking(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_with_masking_mocker + + config_with_masking = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.PERSON), + DPIStandardEntity(type=ProfileEntity.EMAIL), + ] + ) + ] + ) + ) + ) + masked_input = EmbeddingsInput(text="Contact John at john@example.com") + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_with_masking_mocker(service.api_url + '/v2/embeddings'): + response = service.embed(config=config_with_masking, input=masked_input) + + self.assertIsNotNone(response.intermediate_results) + self.assertIn("input_masking", response.intermediate_results) + self.assertIn("MASKED_PERSON", response.intermediate_results["input_masking"]["data"]["masked_input"]) + + def test_embed_with_custom_params(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_mocker + + config_with_params = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails( + name="text-embedding-3-large", + params=EmbeddingsModelParams( + dimensions=256, + encoding_format=EmbeddingsEncodingFormat.FLOAT, + normalize=True + ) + ) + ) + ) + ) + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_mocker(service.api_url + '/v2/embeddings'): + response = service.embed(config=config_with_params, input=self.embeddings_input) + self.assertIsInstance(response, EmbeddingsPostResponse) + + def test_embed_with_input_type(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_mocker + + doc_input = EmbeddingsInput(text="Document content", type=EmbeddingsInputType.DOCUMENT) + query_input = EmbeddingsInput(text="Search query", type=EmbeddingsInputType.QUERY) + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_mocker(service.api_url + '/v2/embeddings'): + # Test document type + response = service.embed(config=self.embeddings_config, input=doc_input) + self.assertIsInstance(response, EmbeddingsPostResponse) + + # Test query type + response = service.embed(config=self.embeddings_config, input=query_input) + self.assertIsInstance(response, EmbeddingsPostResponse) + + def test_embed_timeout_parameter(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE + from unittest.mock import patch + + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE + + timeout_captured = {} + + def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.client, "post", side_effect=capture_request): + service.embed(config=self.embeddings_config, input=self.embeddings_input, timeout=120.0) + self.assertEqual(timeout_captured.get("timeout"), 120.0) + + +class TestEmbeddingsServiceAsync(unittest.IsolatedAsyncioTestCase): + """Tests for OrchestrationService aembed method (async).""" + + def setUp(self): + from tests.mock import get_mocked_ai_core_client, ai_core_ai_api_mocker + self.api_url = "https://api.example.com" + self.proxy_client = get_mocked_ai_core_client(client_id='testembeddingsasyncclient') + self.ai_core_mocker = ai_core_ai_api_mocker + + self.embeddings_config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ) + ) + ) + self.embeddings_input = EmbeddingsInput(text="Hello World!") + + async def test_aembed_single_text(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_mocker + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_mocker(service.api_url + '/v2/embeddings'): + response = await service.aembed(config=self.embeddings_config, input=self.embeddings_input) + + self.assertIsInstance(response, EmbeddingsPostResponse) + self.assertEqual(response.request_id, "emb-test-123") + self.assertEqual(response.final_result.model, "text-embedding-3-large") + + async def test_aembed_batch_texts(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_batch_mocker + + batch_input = EmbeddingsInput(text=["Text 1", "Text 2", "Text 3"]) + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_batch_mocker(service.api_url + '/v2/embeddings'): + response = await service.aembed(config=self.embeddings_config, input=batch_input) + + self.assertEqual(len(response.final_result.data), 3) + + async def test_aembed_with_masking(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import orchestration_embeddings_v2_with_masking_mocker + + config_with_masking = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.PERSON)] + ) + ] + ) + ) + ) + masked_input = EmbeddingsInput(text="Contact John Smith") + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_embeddings_v2_with_masking_mocker(service.api_url + '/v2/embeddings'): + response = await service.aembed(config=config_with_masking, input=masked_input) + + self.assertIsNotNone(response.intermediate_results) + self.assertIn("input_masking", response.intermediate_results) + + async def test_aembed_timeout_parameter(self): + from gen_ai_hub.orchestration_v2.service import OrchestrationService + from tests.mock import GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE + from unittest.mock import AsyncMock, patch + + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return GET_ORCHESTRATION_V2_EMBEDDINGS_RESPONSE + + timeout_captured = {} + + async def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with self.ai_core_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.aembed(config=self.embeddings_config, input=self.embeddings_input, timeout=90.0) + self.assertEqual(timeout_captured.get("timeout"), 90.0) + + +class TestEmbeddingsRequestWithMasking(unittest.TestCase): + """Tests for EmbeddingsRequest with data masking configuration.""" + + def test_request_with_masking_config(self): + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.ANONYMIZATION, + entities=[ + DPIStandardEntity(type=ProfileEntity.PERSON), + DPIStandardEntity(type=ProfileEntity.EMAIL), + ] + ) + ] + ) + ) + ) + input_obj = EmbeddingsInput(text="Contact John at john@example.com") + request = EmbeddingsRequest(config=config, input=input_obj) + + result = request.model_dump() + self.assertIn("masking", result["config"]["modules"]) + masking = result["config"]["modules"]["masking"] + self.assertEqual(len(masking["masking_providers"]), 1) + self.assertEqual(masking["masking_providers"][0]["method"], "anonymization") + + def test_request_with_masking_allowlist(self): + config = EmbeddingsOrchestrationConfig( + modules=EmbeddingsModuleConfigs( + embeddings=EmbeddingsModelConfig( + model=EmbeddingsModelDetails(name="text-embedding-3-large") + ), + masking=MaskingModuleConfig( + masking_providers=[ + MaskingProviderConfig( + method=MaskingMethod.PSEUDONYMIZATION, + entities=[DPIStandardEntity(type=ProfileEntity.ORG)], + allowlist=["SAP", "Microsoft"] + ) + ] + ) + ) + ) + input_obj = EmbeddingsInput(text="SAP partners with Microsoft") + request = EmbeddingsRequest(config=config, input=input_obj) + + result = request.model_dump() + allowlist = result["config"]["modules"]["masking"]["masking_providers"][0]["allowlist"] + self.assertEqual(allowlist, ["SAP", "Microsoft"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/gen/tests/orchestration_v2/test_flat_import.py b/packages/gen/tests/orchestration_v2/test_flat_import.py new file mode 100644 index 0000000..7606ffa --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_flat_import.py @@ -0,0 +1,114 @@ +expected = { + # azure_content_filter + "AzureContentFilter", "AzureContentSafetyInput", "AzureContentSafetyOutput", "AzureThreshold", + + # config + "ModuleConfig", "OrchestrationConfig", "OrchestrationConfigReference", + "CompletionRequestConfigurationReferenceByIdConfigRef", + "CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef", + + # content_filter + "ContentFilterProvider", "ContentFilter", "LlamaGuard38bFilterConfig", + "AzureContentSafetyInputFilterConfig", "AzureContentSafetyOutputFilterConfig", "FilteringStreamOptions", + + # content_filtering + "InputFiltering", "OutputFiltering", "FilteringModuleConfig", + + # data_masking + "DataMaskingProviderName", "MaskingMethod", "ProfileEntity", "DPIMethodConstant", "DPIMethodFabricatedData", + "DPICustomEntity", "DPIStandardEntity", "MaskGroundingInput", "MaskingProviderConfig", "MaskingModuleConfig", + + # document_grounding + "GroundingType", "DataRepositoryType", "DocumentGroundingFilter", "DocumentGroundingPlaceholders", + "DocumentGroundingConfig", "GroundingModuleConfig", "KeyValueListPair", "DocumentMetadataKeyValueListPairs", + "GroundingSearchConfig", + + # embeddings + "EmbeddingsEncodingFormat", "EmbeddingsInputType", "EmbeddingsModelParams", "EmbeddingsModelDetails", + "EmbeddingsModelConfig", "EmbeddingsModuleConfigs", "EmbeddingsOrchestrationConfig", "EmbeddingsInput", + "EmbeddingsUsage", "EmbeddingResult", "EmbeddingsResponse", "EmbeddingsPostResponse", "EmbeddingsRequest", + + # llama_guard_3_filter + "LlamaGuard38bFilter", + + # llm_model_details + "LLMModelDetails", + + # message + "SystemMessage", "UserMessage", "AssistantMessage", "ToolChatMessage", "DeveloperChatMessage", + "ChatMessage", "ResponseChatMessage", "FunctionCall", "MessageToolCall", + + # multimodal_items + "ImageDetailLevel", "TextPart", "ImageUrl", "ImagePart", "ContentPart", "ImageItem", + + # response + "PromptTokensDetails", "CompletionTokensDetails", "TokenUsage", "GenericModuleResult", "TopLogprob", + "ChatCompletionTokenLogprob", "ChoiceLogprobs", "LLMChoice", "StreamFunctionObject", "StreamToolCall", + "StreamDelta", "StreamLLMChoice", "Citation", "LLMModuleResult", "StreamLLMModuleResult", "ModuleResults", + "StreamModuleResults", "SAPAPIError", "SAPAPIErrorStreaming", "CompletionPostResponse", + "StreamCompletionPostResponse", "ErrorResponse", "ErrorResponseStreaming", "OrchestrationResponseWithRetries", + + # response_format + "ResponseFormatText", "ResponseFormatJsonObject", "ResponseFormatJsonSchema", "JSONResponseSchema", + + # streaming + "GlobalStreamOptions", + + # template + "Template", "PromptTemplatingModuleConfig", + + # template_ref + "TemplateRef", "TemplateRefByID", "TemplateRefByScenarioNameVersion", + + # tools + "python_type_to_json_type", "ChatCompletionTool", "FunctionObject", "FunctionTool", "function_tool", + + # translation + "TranslationConfig", "SAPDocumentTranslation", "SAPDocumentTranslationApplyToSelector", + "InputTranslationConfig", "OutputTranslationConfig", "SAPDocumentTranslationInput", + "SAPDocumentTranslationOutput", "TranslationModuleConfig", + + # OrchestrationService + "OrchestrationService", + + # Exceptions + "OrchestrationError", "OrchestrationErrorList" + } + + +def test_flat_import_all(): + import gen_ai_hub.orchestration_v2 as module + assert set(module.__all__) == expected + +def test_flat_and_not_flat_import_by_name(): + from gen_ai_hub.orchestration_v2 import OrchestrationService as service_flat + from gen_ai_hub.orchestration_v2.service import OrchestrationService as service + assert service_flat == service + + from gen_ai_hub.orchestration_v2 import EmbeddingsOrchestrationConfig as embed_config_flat + from gen_ai_hub.orchestration_v2.models.embeddings import EmbeddingsOrchestrationConfig as embed_config + assert embed_config_flat == embed_config + + from gen_ai_hub.orchestration_v2 import OrchestrationConfig as config_flat + from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig as config + assert config_flat == config + + from gen_ai_hub.orchestration_v2 import MaskingModuleConfig as masking_flat + from gen_ai_hub.orchestration_v2.models.data_masking import MaskingModuleConfig as masking + assert masking_flat == masking + + from gen_ai_hub.orchestration_v2 import PromptTemplatingModuleConfig as prompt_flat + from gen_ai_hub.orchestration_v2.models.template import PromptTemplatingModuleConfig as prompt + assert prompt_flat == prompt + + from gen_ai_hub.orchestration_v2 import TranslationModuleConfig as translation_flat + from gen_ai_hub.orchestration_v2.models.translation import TranslationModuleConfig as translation + assert translation_flat == translation + + from gen_ai_hub.orchestration_v2 import GroundingModuleConfig as grounding_flat + from gen_ai_hub.orchestration_v2.models.document_grounding import GroundingModuleConfig as grounding + assert grounding_flat == grounding + + from gen_ai_hub.orchestration_v2 import FilteringModuleConfig as filtering_flat + from gen_ai_hub.orchestration_v2.models.content_filtering import FilteringModuleConfig as filtering + assert filtering_flat == filtering diff --git a/packages/gen/tests/orchestration_v2/test_grounding_v2.py b/packages/gen/tests/orchestration_v2/test_grounding_v2.py new file mode 100644 index 0000000..c39a754 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_grounding_v2.py @@ -0,0 +1,61 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.document_grounding import (DocumentGroundingFilter, DocumentGroundingConfig, + DataRepositoryType, DocumentGroundingPlaceholders, + DocumentMetadataKeyValueListPairs, GroundingType, + GroundingModuleConfig, GroundingSearchConfig) + + +class TestGrounding(unittest.TestCase): + + def test_document_metadata(self): + metadata = DocumentMetadataKeyValueListPairs(key="key", value=["value"], select_mode=["ignoreIfKeyAbsent"]) + metadata_json = metadata.model_dump() + self.assertEqual(metadata_json["key"], "key") + self.assertEqual(metadata_json["value"], ["value"]) + self.assertEqual(metadata_json["select_mode"], ["ignoreIfKeyAbsent"]) + + def test_grounding_filter_search_configuration(self): + search_config = GroundingSearchConfig(max_chunk_count=10) + search_config_json = search_config.model_dump() + self.assertEqual(search_config_json["max_chunk_count"], 10) + self.assertIsNone(search_config_json.get("max_document_count")) + + def test_grounding_filter(self): + grounding_filter = DocumentGroundingFilter(id="id", + data_repository_type=DataRepositoryType.VECTOR, + data_repositories=["46b508c9-e490-4808-893b-b8e3361c4213"], + data_repository_metadata=[{ + "key": "data_repository_key", + "value": ["data_repository_value"], + }], + search_config=GroundingSearchConfig(max_chunk_count=3), + document_metadata=[DocumentMetadataKeyValueListPairs( + key="keyTest", + value=["ValueTest1"], + select_mode=["ignoreIfKeyAbsent"] + )], + chunk_metadata=[{ + "key": "chunk_metadata_key", + "value": ["chunk_metadata_value"], + }] + ) + filter_json = grounding_filter.model_dump() + self.assertEqual(filter_json["id"], "id") + self.assertEqual(filter_json["data_repository_type"], "vector") + + def test_grounding_configuration(self): + filters = [DocumentGroundingFilter(id="id", data_repository_type=DataRepositoryType.VECTOR)] + grounding_config = GroundingModuleConfig( + type=GroundingType.DOCUMENT_GROUNDING_SERVICE, + config=DocumentGroundingConfig( + filters=filters, metadata_params=["metadata_param"], + placeholders=DocumentGroundingPlaceholders(input=["user_query"], output="grounding_response")) + ) + config_json = grounding_config.model_dump() + self.assertEqual(config_json["type"], "document_grounding_service") + self.assertEqual(config_json["config"]["placeholders"]["input"], ["user_query"]) + self.assertEqual(config_json["config"]["placeholders"]["output"], "grounding_response") + self.assertEqual(config_json["config"]["filters"][0]["id"], "id") + self.assertEqual(config_json["config"]["filters"][0]["data_repository_type"], "vector") + self.assertEqual(config_json["config"]["metadata_params"], ["metadata_param"]) diff --git a/packages/gen/tests/orchestration_v2/test_llm_model_details_v2.py b/packages/gen/tests/orchestration_v2/test_llm_model_details_v2.py new file mode 100644 index 0000000..c8cf5ca --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_llm_model_details_v2.py @@ -0,0 +1,36 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails + + +class TestLLModelDetails(unittest.TestCase): + + def test_llm_with_no_parameters(self): + llm = LLMModelDetails(name="gpt-4o-mini") + json_data = llm.model_dump() + self.assertEqual(json_data.get("params", {}), {}) + + def test_llm_custom_parameters(self): + params = {"temperature": 0.7, "max_tokens": 100} + llm = LLMModelDetails(name="gpt-4o-mini", params=params) + json_data = llm.model_dump() + self.assertEqual(json_data["params"], params) + + def test_llm_json_serialization(self): + llm = LLMModelDetails(name="gpt-4o-mini", version="v1", params={"temperature": 0.7}) + expected_dict = { + "name": "gpt-4o-mini", + "version": "v1", + "params": {"temperature": 0.7}, + "max_retries": 2, + "timeout": 600 + } + json_data = llm.model_dump() + self.assertEqual(json_data, expected_dict) + + def test_llm_model_details_with_unsupported_properties(self): + with self.assertRaises(ValueError): + LLMModelDetails(name="gpt-4o-mini", max_retries=10) + + with self.assertRaises(ValueError): + LLMModelDetails(name="gpt-4o-mini", timeout=100000) diff --git a/packages/gen/tests/orchestration_v2/test_request_obj_v2.py b/packages/gen/tests/orchestration_v2/test_request_obj_v2.py new file mode 100644 index 0000000..aa95b1c --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_request_obj_v2.py @@ -0,0 +1,57 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.config import (OrchestrationConfig, ModuleConfig, +CompletionRequestConfigurationReferenceByIdConfigRef, +CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef) +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import UserMessage +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.orchestration_request import CompletionPostRequest + + +class TestOrchestrationRequestV2(unittest.TestCase): + + def setUp(self): + self.template = Template( + template=[UserMessage(content="Hello, World!")] + ) + self.llm = LLMModelDetails(name="gpt-4o-mini") + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, + model=self.llm) + self.module_config = ModuleConfig(prompt_templating=self.prompt_template) + self.config = OrchestrationConfig(modules=self.module_config) + self.config_ref_id = CompletionRequestConfigurationReferenceByIdConfigRef(id="1234567890") + self.config_ref_name = CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef( + name="test", + version="1", + scenario="test" + ) + + def test_with_config(self): + + request = CompletionPostRequest(config=self.config) + json_request = request.model_dump() + self.assertIsNone(json_request.get("config_ref")) + + def test_with_config_ref_by_id(self): + + request = CompletionPostRequest(config_ref=self.config_ref_id) + json_request = request.model_dump() + self.assertEqual(json_request["config_ref"]["id"], self.config_ref_id.id) + self.assertIsNone(json_request.get("config")) + + def test_with_config_ref_by_scenario(self): + request = CompletionPostRequest(config_ref=self.config_ref_name) + json_request = request.model_dump() + self.assertEqual(json_request["config_ref"]["name"], self.config_ref_name.name) + self.assertEqual(json_request["config_ref"]["version"], self.config_ref_name.version) + self.assertEqual(json_request["config_ref"]["scenario"], self.config_ref_name.scenario) + self.assertIsNone(json_request.get("config")) + + def test_with_config_and_config_ref(self): + with self.assertRaises(ValueError): + CompletionPostRequest(config=self.config, config_ref=self.config_ref_id) + + def test_with_no_config_or_config_ref(self): + with self.assertRaises(ValueError): + CompletionPostRequest() \ No newline at end of file diff --git a/packages/gen/tests/orchestration_v2/test_service_v2.py b/packages/gen/tests/orchestration_v2/test_service_v2.py new file mode 100644 index 0000000..da14af7 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_service_v2.py @@ -0,0 +1,476 @@ +import unittest +from typing import cast +from unittest.mock import Mock, patch, AsyncMock + +import httpx + +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationError +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import SystemMessage, UserMessage +from gen_ai_hub.orchestration_v2.models.streaming import GlobalStreamOptions +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService, cache_if_not_none +from tests.mock import ( + get_mocked_ai_core_client, + ai_core_ai_api_mocker, + orchestration_completion_v2_mocker, + orchestration_stream_v2_completion_mocker, + orchestration_v2_stream_completion_mocker_async, + orchestration_deployment_not_found_mocker, + orchestration_too_many_requests_mocker, + GET_ORCHESTRATION_V2_COMPLETION_RESPONSE +) + + +class TestOrchestrationService(unittest.TestCase): + NOT_EXISTENT_DEPLOYMENT_ID = "not_existent" + + def setUp(self): + self.api_url = "https://api.example.com" + self.template = Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "World"} + ) + self.llm = LLMModelDetails(name="gemini-2.0-flash-lite") + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, + model=self.llm) + self.module_config = ModuleConfig(prompt_templating=self.prompt_template) + self.config = OrchestrationConfig(modules=self.module_config) + self.stream_config = OrchestrationConfig(modules=self.module_config, + stream=GlobalStreamOptions(enabled=True)) + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + + def test_caching(self): + + @cache_if_not_none + def func(arg): + func.calls += 1 + return arg + + func.calls = 0 + + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 1) + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 1) + func.cache_clear() + self.assertEqual(func(1), 1) + self.assertEqual(func.calls, 2) + self.assertIsNone(func(None)) + self.assertEqual(func.calls, 3) + self.assertIsNone(func(None)) + self.assertEqual(func.calls, 4) + + def test_initialization_with_empty_api_url(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(proxy_client=self.proxy_client) + self.assertEqual(client.api_url, + "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d7f9c215310f5a11") + + def test_initialization_with_config_name(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(config_id="0152d9f0-694f-4bd2-a287-f7d270c9db60", + proxy_client=self.proxy_client) + self.assertEqual(client.api_url, + "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_config_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(config_name="orchestration-config-2", proxy_client=self.proxy_client) + self.assertEqual(client.api_url, + "https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id="dea20c27f7fe0eca", proxy_client=self.proxy_client) + self.assertEqual(client.api_url, "https://base_url/v2/inference/deployments/dea20c27f7fe0eca") + + def test_initialization_with_non_existing_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) + self.assertIn(self.NOT_EXISTENT_DEPLOYMENT_ID, client.api_url) + + def test_run_with_non_existing_deployment_id(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(deployment_id=self.NOT_EXISTENT_DEPLOYMENT_ID, proxy_client=self.proxy_client) + with orchestration_deployment_not_found_mocker(client.api_url + '/v2/completion'): + with self.assertRaises(httpx.HTTPStatusError): + client.run(config=self.config) + + def test_run_without_config(self): + service = OrchestrationService(api_url=self.api_url, proxy_client=Mock()) + + with self.assertRaises(ValueError): + service.run() + + def test_run_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_completion_v2_mocker(client.api_url + '/v2/completion'): + client.run(config=self.config) + # test config as a list + client.run(config=OrchestrationConfig(modules=[self.module_config])) + # empty config list raises error + with self.assertRaises(ValueError): + client.run(config=OrchestrationConfig(modules=[])) + + def test_usage_details(self): + """Simple check that usage includes provider-specific token details blocks.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_completion_v2_mocker(client.api_url + '/v2/completion'): + response = client.run(config=self.config) + + usage_dict = response.final_result.usage.model_dump() + self.assertIn("prompt_tokens_details", usage_dict) + self.assertIn("completion_tokens_details", usage_dict) + self.assertIsNotNone(usage_dict["prompt_tokens_details"]) + self.assertIsNotNone(usage_dict["completion_tokens_details"]) + + def test_stream_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_stream_v2_completion_mocker(client.api_url + '/v2/completion'): + txt = '' + for chunk in client.stream(config=self.stream_config): + if chunk.final_result.choices: + txt += chunk.final_result.choices[0].delta.content + self.assertEqual(txt, 'This confirms receipt of the system message: "Hello, World!\n') + + def test_too_many_requests(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_too_many_requests_mocker(client.api_url + '/v2/completion'): + with self.assertRaises(OrchestrationError) as context: + client.run(config=self.config) + self.assertIn('X-Custom-Header', cast(OrchestrationError, context.exception).headers) + + def test_retry_backoff_with_retry_after_header(self): + """Test that retry backoff respects Retry-After header and applies jitter correctly.""" + import time + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + call_times = [] + retry_counts = [] + + # Patch the handle_retry method to track calls and timing + original_handle_retry = client.handle_retry + + def track_retry(*args, **kwargs): + call_times.append(time.time()) + retry_counts.append(args[0]) # retry_count is first arg + return original_handle_retry(*args, **kwargs) + + with orchestration_too_many_requests_mocker(client.api_url + '/v2/completion'): + with patch.object(client, 'handle_retry', side_effect=track_retry): + with self.assertRaises(OrchestrationError) as context: + client.run_with_retries(config=self.config, max_retries=1, base_delay=1.0) + + error = cast(OrchestrationError, context.exception) + + # Verify retry was attempted (with max_retries=1, we get 1 retry attempt) + # The retry_count starts at 1 (after initial failure) + self.assertEqual(len(retry_counts), 1, "Expected 1 retry attempt") + self.assertEqual(retry_counts, [0], "Expected retry count of 0 for first retry") + + # Verify delay is positive + if len(call_times) >= 1: + self.assertGreater(len(call_times), 0, "Expected at least one retry delay measurement") + + # Verify error tracking + self.assertIn('X-Custom-Header', error.headers) + + def test_handle_retry_with_retry_after(self): + """Test that handle_retry uses exponential backoff with Retry-After header.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error without Retry-After header + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({"Retry-After": "3"}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Collect delays for multiple retries + delays = [] + for retry_count in range(3): + delay = client.handle_retry(retry_count=retry_count, base_delay=1.0, error=error, max_retries=5) + delays.append(delay) + + # Verify all delays are positive + for delay in delays: + self.assertGreater(delay, 0.0, "All delays should be positive") + self.assertLessEqual(delay, 60.0, "All delays should respect max_delay") + + def test_handle_retry_without_retry_after(self): + """Test that handle_retry uses exponential backoff when no Retry-After header.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error without Retry-After header + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Collect delays for multiple retries + delays = [] + for retry_count in range(3): + delay = client.handle_retry(retry_count=retry_count, base_delay=1.0, error=error, max_retries=5) + delays.append(delay) + + # Verify all delays are positive + for delay in delays: + self.assertGreater(delay, 0.0, "All delays should be positive") + self.assertLessEqual(delay, 60.0, "All delays should respect max_delay") + + def test_handle_retry_max_retries_exceeded(self): + """Test that handle_retry raises error when max_retries is exceeded.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Create a mock error + response = Mock(spec=httpx.Response) + response.status_code = 429 + response.headers = httpx.Headers({}) + response.text = "Too Many Requests" + response.request = Mock() + + error = httpx.HTTPStatusError("429 Too Many Requests", request=response.request, response=response) + + # Mock _should_retry to return True so we test the retry_count >= max_retries condition + with patch.object(client, '_should_retry', return_value=True): + # Test that it raises when retry_count >= max_retries + # Need to call handle_retry within an exception context since it uses bare 'raise' + try: + raise error + except httpx.HTTPStatusError as e: + with self.assertRaises(httpx.HTTPStatusError) as context: + client.handle_retry(retry_count=3, base_delay=1.0, error=e, max_retries=3) + + # Verify retries attribute was set + raised_error = context.exception + self.assertEqual(raised_error.retries, 3, "Expected retries attribute to be set") + + def test_calculate_backoff_with_min_delay(self): + """Test _calculate_backoff behavior with min_delay parameter.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Test with min_delay (simulating Retry-After header) + retry_after_delay = 5.0 + + # Case 1: When min_delay > exponential delay, returns capped (not min_delay) + # For retry_count=0, base_delay=1.0: exp = 1.0 * 2^0 = 1.0 + # With min_delay=5.0, max_delay=60.0: capped = min(1.0, 60.0) = 1.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) >= capped (1.0), returns capped = 1.0 + delay1 = client._calculate_backoff(retry_count=0, base_delay=1.0, min_delay=retry_after_delay, + max_delay=60.0) + self.assertEqual(delay1, 1.0, "When min_delay > exp_delay, should return capped exponential value") + + # Case 2: When exponential delay > min_delay, applies jitter between min_delay and capped + # For retry_count=3, base_delay=1.0: exp = 1.0 * 2^3 = 8.0 + # With min_delay=5.0, max_delay=60.0: capped = min(8.0, 60.0) = 8.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) < capped (8.0), returns random.uniform(5.0, 8.0) + delays = [ + client._calculate_backoff(retry_count=3, base_delay=1.0, min_delay=retry_after_delay, max_delay=60.0) + for _ in range(20)] + + # All delays should be between min_delay and the exponential cap + for delay in delays: + self.assertGreaterEqual(delay, retry_after_delay, "Delay should be at least min_delay") + self.assertLessEqual(delay, 8.0, "Delay should not exceed exponential cap for retry_count=3") + + # Verify jitter produces variance + unique_delays = len(set(delays)) + self.assertGreater(unique_delays, 1, "Expected jitter to produce varying delays") + + # Case 3: When min_delay == max_delay and exp > min_delay + # For retry_count=10, base_delay=1.0: exp = 1.0 * 2^10 = 1024.0 + # With min_delay=5.0, max_delay=5.0: capped = min(1024.0, 5.0) = 5.0 + # lower = max(0.0, 5.0) = 5.0 + # Since lower (5.0) >= capped (5.0), returns capped = 5.0 + delay3 = client._calculate_backoff(retry_count=10, base_delay=1.0, min_delay=retry_after_delay, + max_delay=retry_after_delay) + self.assertEqual(delay3, retry_after_delay, "When min_delay == max_delay, should return that value") + + # Case 4: Test without min_delay (standard behavior) + # For retry_count=2, base_delay=1.0: exp = 1.0 * 2^2 = 4.0 + # With min_delay=0.0, max_delay=60.0: capped = min(4.0, 60.0) = 4.0 + # lower = max(0.0, 0.0) = 0.0 + # Returns random.uniform(0.0, 4.0) + delays_no_min = [ + client._calculate_backoff(retry_count=2, base_delay=1.0, min_delay=0.0, max_delay=60.0) + for _ in range(20)] + + for delay in delays_no_min: + self.assertGreaterEqual(delay, 0.0, "Delay should be non-negative") + self.assertLessEqual(delay, 4.0, "Delay should not exceed exponential value") + + # Verify variance with no min_delay + self.assertGreater(len(set(delays_no_min)), 1, "Expected jitter without min_delay") + + def test_calculate_backoff_exponential_progression(self): + """Test _calculate_backoff produces exponential backoff progression.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + # Collect average delays for different retry counts + num_samples = 50 + avg_delays = [] + + for retry_count in range(5): + delays = [client._calculate_backoff(retry_count=retry_count, base_delay=1.0, min_delay=0.0) + for _ in range(num_samples)] + avg_delays.append(sum(delays) / len(delays)) + + # Verify exponential growth in average delays + for i in range(len(avg_delays) - 1): + self.assertLess(avg_delays[i], avg_delays[i + 1], + f"Expected retry {i + 1} to have higher average delay than retry {i}") + + # Verify capping at max_delay + large_delays = [client._calculate_backoff(retry_count=10, base_delay=1.0, max_delay=60.0) + for _ in range(10)] + for delay in large_delays: + self.assertLessEqual(delay, 60.0, "Delay should be capped at max_delay") + + def test_calculate_backoff_custom_max_delay(self): + """Test _calculate_backoff respects custom max_delay.""" + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + + custom_max = 10.0 + delays = [client._calculate_backoff(retry_count=5, base_delay=1.0, max_delay=custom_max) + for _ in range(20)] + + for delay in delays: + self.assertLessEqual(delay, custom_max, "Delay should respect custom max_delay") + self.assertGreaterEqual(delay, 0.0, "Delay should be non-negative") + + def test_timeout_client_request(self): + class FakeResponse: + def raise_for_status(self): + pass # No-op for mock tests + + def json(self): + return GET_ORCHESTRATION_V2_COMPLETION_RESPONSE + + timeout_captured = {} # Capture the request kwargs from mocked post method + + def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + # no timeout set in both httpx client and request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + + # timeout set in httpx client, not overwritten in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), 99.0) + + # timeout overwrite in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.client, "post", side_effect=capture_request): + service.run(config=self.config, timeout=77.0) + self.assertEqual(timeout_captured.get("timeout"), 77.0) + + +class TestOrchestrationServiceAsync(unittest.IsolatedAsyncioTestCase): + + def setUp(self): + self.api_url = "https://api.example.com" + self.template = Template( + template=[ + SystemMessage(content="This is a system message."), + UserMessage(content="Hello, {{?name}}!"), + ], + defaults={"name": "World"} + ) + self.llm = LLMModelDetails(name="gemini-2.0-flash-lite") + self.prompt_template = PromptTemplatingModuleConfig(prompt=self.template, + model=self.llm) + self.module_config = ModuleConfig(prompt_templating=self.prompt_template) + self.config = OrchestrationConfig(modules=self.module_config) + self.stream_config = OrchestrationConfig(modules=self.module_config, + stream=GlobalStreamOptions(enabled=True)) + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + + async def test_async_run_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_completion_v2_mocker(client.api_url + '/v2/completion'): + await client.arun(config=self.config) + + async def test_async_timeout_client_request(self): + class FakeResponse: + def raise_for_status(self): + pass # No-op for mock tests + + def json(self): + return GET_ORCHESTRATION_V2_COMPLETION_RESPONSE + + timeout_captured = {} # Capture the request kwargs from mocked post method + + async def capture_request(*args, **kwargs): + nonlocal timeout_captured + timeout_captured = kwargs + return FakeResponse() + + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + # no timeout set in both httpx client and request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), httpx.USE_CLIENT_DEFAULT) + + # timeout set in httpx client, not overwritten in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config) + self.assertEqual(timeout_captured.get("timeout"), 99.0) + + # timeout overwrite in request + service = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client, timeout=99.0) + with patch.object(service.async_client, "post", new=AsyncMock(side_effect=capture_request)): + await service.arun(config=self.config, timeout=77.0) + self.assertEqual(timeout_captured.get("timeout"), 77.0) + + async def test_async_stream_with_config(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + async with orchestration_v2_stream_completion_mocker_async(client.api_url + '/v2/completion'): + txt = '' + async for chunk in await client.astream(config=self.stream_config): + if chunk.final_result.choices: + txt += chunk.final_result.choices[0].delta.content + self.assertEqual(txt, 'This confirms receipt of the system message: "Hello, World!\n') + + async def test_async_run_with_retries(self): + with ai_core_ai_api_mocker(auth_url=self.proxy_client.auth_url, base_url=self.proxy_client.base_url): + client = OrchestrationService(api_url=self.api_url, proxy_client=self.proxy_client) + with orchestration_too_many_requests_mocker(client.api_url + '/v2/completion'): + with self.assertRaises(OrchestrationError) as context: + await client.arun_with_retries(config=self.config, max_retries=1, base_delay=1.0) + self.assertIn('X-Custom-Header', cast(OrchestrationError, context.exception).headers) diff --git a/packages/gen/tests/orchestration_v2/test_sse_client_v2.py b/packages/gen/tests/orchestration_v2/test_sse_client_v2.py new file mode 100644 index 0000000..366ba9f --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_sse_client_v2.py @@ -0,0 +1,324 @@ +import json +import unittest +from unittest.mock import Mock + +from httpx import Response + +from gen_ai_hub.orchestration_v2.sse_client import AsyncSSEClient +from gen_ai_hub.orchestration_v2.exceptions import OrchestrationErrorList + + +def create_valid_event(token="test"): + """ + Create a valid event structure matching the actual SSE response format. + + See tests/mock.py for structure details. + """ + return { + "request_id": "test-request-id", + "intermediate_results": { + "input_filtering": None, + "output_filtering": None, + "input_masking": None, + "llm": { + "id": "test-id", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": None, + "logprobs": None + } + ], + "system_fingerprint": None + }, + "templating": None, + "output_unmasking": None + }, + "final_result": { + "id": "test-id", + "object": "chat.completion.chunk", + "created": 1738573708, + "model": "gemini-2.0-flash", + "choices": [ + { + "index": 0, + "delta": {"content": token, "role": "assistant"}, + "finish_reason": None, + "logprobs": None + } + ], + "system_fingerprint": None + } + } + + +class TestSSEClientBuffering(unittest.IsolatedAsyncioTestCase): + """Tests for AsyncSSEClient with manual buffering using aiter_text().""" + + def setUp(self): + """Set up test fixtures.""" + self.event_prefix = "data: " + self.final_message = "[DONE]" + + async def test_simple_complete_lines(self): + """Test that complete lines are processed correctly.""" + event1 = create_valid_event("test1") + event2 = create_valid_event("test2") + + chunks = [ + f"data: {json.dumps(event1)}\n", + f"data: {json.dumps(event2)}\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[1].request_id, "test-request-id") + + async def test_json_split_across_chunks(self): + """Test that JSON split across multiple chunks is handled correctly.""" + event = create_valid_event("split test") + event_str = json.dumps(event) + + # Split the JSON in the middle + mid = len(event_str) // 2 + part1 = event_str[:mid] + part2 = event_str[mid:] + + chunks = [ + f"data: {part1}", # First part without newline + f"{part2}\n", # Second part with newline + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].intermediate_results.llm.choices[0].delta.content, "split test") + + async def test_multiple_events_in_single_chunk(self): + """Test that multiple events in a single chunk are processed correctly.""" + event1 = create_valid_event("first") + event2 = create_valid_event("second") + + chunks = [ + f"data: {json.dumps(event1)}\ndata: {json.dumps(event2)}\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0].intermediate_results.llm.choices[0].delta.content, "first") + self.assertEqual(results[1].intermediate_results.llm.choices[0].delta.content, "second") + + async def test_final_message_stops_iteration(self): + """Test that [DONE] message stops iteration.""" + event1 = create_valid_event("before done") + + chunks = [ + f"data: {json.dumps(event1)}\n", + f"data: {self.final_message}\n", + f"data: {json.dumps(event1)}\n", # This should not be processed + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + # Should only have one result before [DONE] + self.assertEqual(len(results), 1) + self.assertEqual(results[0].intermediate_results.llm.choices[0].delta.content, "before done") + + async def test_empty_lines_ignored(self): + """Test that empty lines are ignored.""" + event = create_valid_event("test") + + chunks = [ + "\n", + f"data: {json.dumps(event)}\n", + "\n", + "\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_lines_without_event_prefix_ignored(self): + """Test that lines without the event prefix are ignored.""" + event = create_valid_event("test") + + chunks = [ + "invalid line\n", + f"data: {json.dumps(event)}\n", + "another invalid line\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_partial_line_at_end_of_stream(self): + """Test that a partial line at the end of the stream is processed.""" + event = create_valid_event("final") + + # Last chunk has no newline + chunks = [ + f"data: {json.dumps(event)}", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].intermediate_results.llm.choices[0].delta.content, "final") + + async def test_very_small_chunks(self): + """Test handling of very small chunks (simulating slow network).""" + event = create_valid_event("test") + event_str = f"data: {json.dumps(event)}\n" + + # Split into very small chunks (2 characters each) + chunks = [event_str[i:i + 2] for i in range(0, len(event_str), 2)] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + + async def test_complex_json_with_nested_objects(self): + """Test handling of complex nested JSON objects split across chunks.""" + event = create_valid_event("Hello") + event_str = json.dumps(event) + + # Split the JSON across multiple chunks + chunk_size = 20 + parts = [event_str[i:i + chunk_size] for i in range(0, len(event_str), chunk_size)] + + chunks = [f"data: {parts[0]}"] + for part in parts[1:-1]: + chunks.append(part) + chunks.append(f"{parts[-1]}\n") + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + results = [] + async for result in client._internal_iterator(): + results.append(result) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0].request_id, "test-request-id") + self.assertEqual(results[0].intermediate_results.llm.choices[0].delta.content, "Hello") + + async def test_error_event_with_list_payload_raises_orchestration_error(self): + + error_event = { + "error": [ + { + "request_id": "req-1", + "message": "first error", + "code": "E_FIRST", + "location": "llm", + }, + { + "request_id": "req-1", + "message": "second error", + "code": "E_SECOND", + "location": "templating", + }, + ] + } + + chunks = [ + f"data: {json.dumps(error_event)}\n", + ] + + mock_response = Mock(spec=Response) + mock_response.aiter_text = Mock(return_value=self._async_generator(chunks)) + + client = AsyncSSEClient(mock_response, self.event_prefix, self.final_message) + client._response = mock_response + + with self.assertRaises(OrchestrationErrorList) as ctx: + async for _ in client._internal_iterator(): + self.fail("Iterator must not yield results when error event is received") + + exc = ctx.exception + + self.assertIsInstance(exc.errors, list) + self.assertEqual(len(exc.errors), 2) + self.assertEqual(exc.errors[0].code, "E_FIRST") + self.assertEqual(exc.errors[1].code, "E_SECOND") + + async def _async_generator(self, items): + """Helper to create async generator from list.""" + for item in items: + yield item + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/orchestration_v2/test_template_ref_v2.py b/packages/gen/tests/orchestration_v2/test_template_ref_v2.py new file mode 100644 index 0000000..f2d9b2b --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_template_ref_v2.py @@ -0,0 +1,35 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.template_ref import TemplateRef, TemplateRefByID, TemplateRefByScenarioNameVersion + + +class TestTemplateRef(unittest.TestCase): + + def test_creates_instance_from_id(self): + template_ref = TemplateRef(template_ref=TemplateRefByID(id="test_template_id")) + self.assertEqual( + template_ref.model_dump(), + {"template_ref": {"id": "test_template_id", "scope": "tenant"}}) + + def test_creates_instance_from_tuple(self): + template_ref = TemplateRefByScenarioNameVersion(scenario="test_scenario", + name="test_name", + version="test_version", + scope="resource_group") + template=TemplateRef(template_ref=template_ref) + self.assertEqual(template_ref.scenario, "test_scenario") + self.assertEqual(template_ref.name, "test_name") + self.assertEqual(template_ref.version, "test_version") + self.assertEqual(template.model_dump(), {"template_ref": + {"scenario": "test_scenario", + "name": "test_name", + "version": "test_version", + "scope": "resource_group"} + } + ) + def test_creates_instance_with_unsupported_scope(self): + with self.assertRaises(ValueError): + TemplateRefByScenarioNameVersion(scenario="test_scenario", + name="test_name", + version="test_version", + scope="unsupported_scope") \ No newline at end of file diff --git a/packages/gen/tests/orchestration_v2/test_template_v2.py b/packages/gen/tests/orchestration_v2/test_template_v2.py new file mode 100644 index 0000000..3139566 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_template_v2.py @@ -0,0 +1,251 @@ +import base64 +import os +import tempfile +import unittest + +from pydantic import ValidationError + +from gen_ai_hub.orchestration_v2.models.message import ( + Role, + SystemMessage, + UserMessage, + AssistantMessage, +) +from gen_ai_hub.orchestration_v2.models.multimodal_items import ImageItem, ImageDetailLevel +from gen_ai_hub.orchestration_v2.models.response_format import ( + ResponseFormatType, + ResponseFormatText, + ResponseFormatJsonObject, + ResponseFormatJsonSchema, + JSONResponseSchema +) +from gen_ai_hub.orchestration_v2.models.template import Template + + +class TestTemplate(unittest.TestCase): + def test_template_with_defaults(self): + messages = [ + SystemMessage(content="You are a helpful assistant!"), + UserMessage(content="Hello, {{?name}}!"), + AssistantMessage(content="How can I help you today?"), + ] + defaults = {"name": "World"} + template = Template(template=messages, defaults=defaults) + + json_data = template.model_dump() + self.assertEqual(json_data["defaults"], {"name": "World"}) + self.assertEqual(len(json_data["template"]), len(messages)) + self.assertEqual(json_data["template"][1]["content"], "Hello, {{?name}}!") + + def test_template_without_defaults(self): + messages = [UserMessage(content="Simple message")] + template = Template(template=messages) + + json_data = template.model_dump() + self.assertIsNone(json_data.get("defaults")) + self.assertEqual(len(json_data["template"]), 1) + + def test_template_without_response_format(self): + messages = [UserMessage(content="Simple message")] + template = Template(template=messages) + + json_data = template.model_dump() + self.assertNotIn("response_format", json_data) + + def test_template_with_response_format_text(self): + messages = [UserMessage(content="Simple message")] + template = Template(template=messages, response_format=ResponseFormatText()) + + json_data = template.model_dump() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.TEXT) + + def test_template_with_response_format_json_object(self): + messages = [UserMessage(content="Simple message")] + template = Template(template=messages, response_format=ResponseFormatJsonObject()) + + json_data = template.model_dump() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.JSON_OBJECT) + + def test_template_with_response_format_json_schema(self): + messages = [UserMessage(content="Simple message")] + json_schema_example = { + "$id": "someid", + "$schema": "someshema", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + } + } + } + + response_format = ResponseFormatJsonSchema(json_schema=JSONResponseSchema(name="test", + description="desc", + schema=json_schema_example, + strict=True)) + template = Template(template=messages, response_format=response_format) + + json_data = template.model_dump() + self.assertEqual(json_data["response_format"]["type"], ResponseFormatType.JSON_SCHEMA) + self.assertEqual(json_data["response_format"]["json_schema"]["name"], "test") + self.assertTrue(json_data["response_format"]["json_schema"]["strict"]) + self.assertEqual(json_data["response_format"]["json_schema"]["schema"], json_schema_example) + + def test_response_format_name_too_long(self): + name_too_long = "ThisIsAveryLongNameLongerThenExpectedItShouldBeMaximum64Characters" + try: + JSONResponseSchema(name=name_too_long, schema={}) + self.fail("Expected the validation to fail due to length of name") + except ValidationError: + pass + + +class TestTemplateWithTools(unittest.TestCase): + def test_template_with_tool_dict(self): + messages = [UserMessage(content="Say hello")] + tool_dict = { + "type": "function", + "function": { + "name": "hello", + "description": "Say hello", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False + }, + "strict": False + } + } + template = Template(template=messages, tools=[tool_dict]) + json_data = template.model_dump() + self.assertIn("tools", json_data) + self.assertEqual(json_data["tools"][0], tool_dict) + + def test_template_with_function_tool(self): + from gen_ai_hub.orchestration_v2.models.tools import function_tool + + @function_tool() + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + messages = [UserMessage(content="Add two numbers")] + template = Template(template=messages, tools=[add]) + json_data = template.model_dump() + self.assertIn("tools", json_data) + tool = json_data["tools"][0] + self.assertEqual(tool["type"], "function") + self.assertEqual(tool["function"]["name"], "add") + self.assertEqual(tool["function"]["description"], "Add two numbers.") + self.assertIn("a", tool["function"]["parameters"]["properties"]) + self.assertIn("b", tool["function"]["parameters"]["properties"]) + + def test_template_with_multiple_tools(self): + from gen_ai_hub.orchestration_v2.models.tools import function_tool + + @function_tool() + def foo(x: int) -> int: + """Foo.""" + return x + + @function_tool() + def bar(y: str) -> str: + """Bar.""" + return y + + messages = [UserMessage(content="Test multiple tools")] + template = Template(template=messages, tools=[foo, bar]) + json_data = template.model_dump() + self.assertIn("tools", json_data) + self.assertEqual(len(json_data["tools"]), 2) + self.assertEqual(json_data["tools"][0]["function"]["name"], "foo") + self.assertEqual(json_data["tools"][1]["function"]["name"], "bar") + + def test_template_with_plain_function_raises(self): + def plain_func(a: int) -> int: + return a + + messages = [UserMessage(content="Test plain function")] + with self.assertRaises(ValidationError): + Template(template=messages, tools=[plain_func]) + + +class TestUserMessageMultimodal(unittest.TestCase): + def test_user_message_with_single_string(self): + msg = UserMessage(content="Hello world!") + expected = { + "role": Role.USER, + "content": "Hello world!" + } + self.assertEqual(msg.model_dump(), expected) + + def test_user_message_with_list_of_strings(self): + msg = UserMessage(content=["Hello", "World"]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": "World"} + ] + } + self.assertEqual(msg.model_dump(), expected) + + def test_user_message_with_string_and_image(self): + img = ImageItem(url="https://example.com/image.png") + msg = UserMessage(content=["Describe this image:", img]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "Describe this image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/image.png" + }} + ] + } + self.assertEqual(msg.model_dump(), expected) + + def test_user_message_with_multiple_images_and_text(self): + img1 = ImageItem(url="https://example.com/1.png", detail=ImageDetailLevel.LOW) + img2 = ImageItem(url="https://example.com/2.png", detail=ImageDetailLevel.HIGH) + msg = UserMessage(content=["First image:", img1, "Second image:", img2]) + expected = { + "role": Role.USER, + "content": [ + {"type": "text", "text": "First image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/1.png", + "detail": ImageDetailLevel.LOW + }}, + {"type": "text", "text": "Second image:"}, + {"type": "image_url", "image_url": { + "url": "https://example.com/2.png", + "detail": ImageDetailLevel.HIGH + }} + ] + } + self.assertEqual(msg.model_dump(), expected) + + def test_image_item_from_file(self): + with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: + tmp.write(b"not a real image") + tmp_path = tmp.name + + try: + item = ImageItem.from_file(tmp_path) + self.assertTrue(item.url.startswith("data:image/png;base64,")) + # Check that the base64 part decodes to the original file content + encoded = item.url.split(",", 1)[1] + with open(tmp_path, "rb") as f: + original = f.read() + decoded = base64.b64decode(encoded) + + self.assertEqual(decoded, original) + finally: + os.unlink(tmp_path) diff --git a/packages/gen/tests/orchestration_v2/test_tools_v2.py b/packages/gen/tests/orchestration_v2/test_tools_v2.py new file mode 100644 index 0000000..036c269 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_tools_v2.py @@ -0,0 +1,124 @@ +import asyncio +import unittest +from typing import Optional + +from gen_ai_hub.orchestration_v2.models.tools import FunctionTool, function_tool + + +class TestFunctionTool(unittest.TestCase): + def test_from_function_basic(self): + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tool = FunctionTool.from_function(add) + self.assertEqual(tool.function.name, "add") + self.assertEqual(tool.function.description, "Add two numbers.") + self.assertIn("a", tool.function.parameters["properties"]) + self.assertIn("b", tool.function.parameters["properties"]) + self.assertIn("a", tool.function.parameters["required"]) + self.assertIn("b", tool.function.parameters["required"]) + self.assertEqual(tool.function.parameters["properties"]["a"]["type"], "number") + self.assertEqual(tool.function.parameters["properties"]["b"]["type"], "number") + self.assertEqual(tool.execute(a=2, b=3), 5) + + def test_from_function_optional(self): + def greet(name: str, title: Optional[str] = None) -> str: + """Greet a person.""" + return f"Hello, {title + ' ' if title else ''}{name}" + + tool = FunctionTool.from_function(greet) + self.assertEqual(tool.function.name, "greet") + self.assertIn("title", tool.function.parameters["properties"]) + self.assertNotIn("title", tool.function.parameters["required"]) + self.assertTrue(tool.function.parameters["properties"]["title"]["nullable"]) + self.assertEqual(tool.execute(name="Alice"), "Hello, Alice") + self.assertEqual(tool.execute(name="Alice", title="Dr."), "Hello, Dr. Alice") + + def test_decorator(self): + @function_tool() + def echo(msg: str) -> str: + """Echo a message.""" + return msg + + self.assertIsInstance(echo, FunctionTool) + self.assertEqual(echo.function.name, "echo") + self.assertEqual(echo.execute(msg="hi"), "hi") + + def test_strict_mode(self): + def foo(x: int) -> int: + """Foo.""" + return x + + tool = FunctionTool.from_function(foo, strict=True) + with self.assertRaises(ValueError): + tool.execute(x=1, y=2) # y is not a valid parameter + + def test_missing_type_hint(self): + def no_type(a, b: int) -> int: + """No type for a.""" + return b + + with self.assertRaises(TypeError): + FunctionTool.from_function(no_type) + + def test_description_precedence(self): + # Case 1: No description provided, should use docstring + def sample(a: int) -> int: + """This is the docstring.""" + return a + + tool1 = FunctionTool.from_function(sample) + self.assertEqual(tool1.function.description, "This is the docstring.") + self.assertIn("description", tool1.model_dump()["function"]) + + # Case 2: Description provided, should take precedence over docstring + tool2 = FunctionTool.from_function(sample, description="Explicit description.") + self.assertEqual(tool2.function.description, "Explicit description.") + self.assertIn("description", tool2.model_dump()["function"]) + self.assertEqual(tool2.model_dump()["function"]["description"], "Explicit description.") + + # Case 3: No docstring and no description, description should not be in dict + @function_tool + def no_desc(a: int) -> int: + return a + + tool3 = no_desc + self.assertIsNone(tool3.function.description) + self.assertNotIn("description", tool3.model_dump()["function"]) + + +class TestFunctionToolAsync(unittest.IsolatedAsyncioTestCase): + async def test_async_function_tool(self): + async def async_add(a: int, b: int) -> int: + """Add two numbers asynchronously.""" + await asyncio.sleep(0.01) + return a + b + + tool = FunctionTool.from_function(async_add) + result = await tool.aexecute(a=2, b=3) + self.assertEqual(result, 5) + + async def test_async_decorator(self): + @function_tool() + async def async_echo(msg: str) -> str: + """Echo a message asynchronously.""" + await asyncio.sleep(0.01) + return msg + + self.assertIsInstance(async_echo, FunctionTool) + result = await async_echo.aexecute(msg="hi") + self.assertEqual(result, "hi") + + async def test_strict_mode_async(self): + async def foo(x: int) -> int: + """Async foo.""" + return x + + tool = FunctionTool.from_function(foo, strict=True) + result = await tool.aexecute(x=42) + self.assertEqual(result, 42) + + # This should raise ValueError because 'y' is not a valid parameter + with self.assertRaises(ValueError): + await tool.aexecute(x=1, y=2) diff --git a/packages/gen/tests/orchestration_v2/test_translation_v2.py b/packages/gen/tests/orchestration_v2/test_translation_v2.py new file mode 100644 index 0000000..c637655 --- /dev/null +++ b/packages/gen/tests/orchestration_v2/test_translation_v2.py @@ -0,0 +1,120 @@ +import unittest + +from gen_ai_hub.orchestration_v2.models.translation import (TranslationModuleConfig, SAPDocumentTranslationInput, + SAPDocumentTranslationOutput, InputTranslationConfig, + OutputTranslationConfig, + SAPDocumentTranslationApplyToSelector, TranslationConfig, + SAPDocumentTranslation) + +class TestTranslation(unittest.TestCase): + def test_translation_module_config(self): + translation_config = TranslationModuleConfig( + input=SAPDocumentTranslationInput( + config=InputTranslationConfig( + source_language="en-US", + target_language="de-DE" + ) + ), + output=SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="de-DE", + target_language="fr-FR" + ) + ) + ) + config_dict = translation_config.model_dump() + self.assertEqual(config_dict["input"]["config"]["source_language"], "en-US") + self.assertEqual(config_dict["input"]["config"]["target_language"], "de-DE") + self.assertEqual(config_dict["input"]["type"], "sap_document_translation") + self.assertEqual(config_dict["output"]["config"]["source_language"], "de-DE") + self.assertEqual(config_dict["output"]["config"]["target_language"], "fr-FR") + self.assertEqual(config_dict["output"]["type"], "sap_document_translation") + + + def test_only_input_translation_module(self): + translation_config = TranslationModuleConfig( + input=SAPDocumentTranslationInput( + config=InputTranslationConfig( + source_language="en-US", + apply_to=[SAPDocumentTranslationApplyToSelector( + category="placeholders", + items=["user_input"], + source_language="en-US" + )], + target_language="de-DE" + ), + )) + config_dict = translation_config.model_dump() + + self.assertEqual(config_dict["input"]["type"], "sap_document_translation") + self.assertEqual(config_dict["input"]["config"]["source_language"], "en-US") + self.assertEqual(config_dict["input"]["config"]["target_language"], "de-DE") + self.assertEqual(config_dict["input"]["config"]["apply_to"][0]["category"], "placeholders") + self.assertEqual(config_dict["input"]["config"]["apply_to"][0]["items"], ["user_input"]) + self.assertEqual(config_dict["input"]["config"]["apply_to"][0]["source_language"], "en-US") + + self.assertIsNone(config_dict.get("output"), "Output translation module should be None.") + + def test_only_output_translation_module(self): + translation_config = TranslationModuleConfig( + output=SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="en-US", + target_language="de-DE" + ) + ), + ) + config_dict = translation_config.model_dump() + + self.assertEqual(config_dict["output"]["type"], "sap_document_translation") + self.assertEqual(config_dict["output"]["config"]["source_language"], "en-US") + self.assertEqual(config_dict["output"]["config"]["target_language"], "de-DE") + + self.assertIsNone(config_dict.get("intput"), "Output translation module should be None.") + + def test_only_output_translation_module_target_language_not_str(self): + translation_config = TranslationModuleConfig( + output=SAPDocumentTranslationOutput( + config=OutputTranslationConfig( + source_language="en-US", + target_language=SAPDocumentTranslationApplyToSelector( + category="placeholders", + items=["user_input"], + source_language="en-US" + ) + ) + ), + ) + config_dict = translation_config.model_dump() + + self.assertEqual(config_dict["output"]["type"], "sap_document_translation") + self.assertEqual(config_dict["output"]["config"]["source_language"], "en-US") + self.assertEqual(config_dict["output"]["config"]["target_language"]["category"], "placeholders") + self.assertEqual(config_dict["output"]["config"]["target_language"]["items"], ["user_input"]) + self.assertEqual(config_dict["output"]["config"]["target_language"]["source_language"], "en-US") + + self.assertIsNone(config_dict.get("intput"), "Output translation module should be None.") + +class TestTranslationBackwardCompatibility(unittest.TestCase): + def test_translation_module_config_backward_compatibility(self): + translation_config = TranslationModuleConfig( + input=SAPDocumentTranslation( + config=TranslationConfig( + source_language="en-US", + target_language="de-DE" + ) + ), + output=SAPDocumentTranslation( + config=TranslationConfig( + source_language="de-DE", + target_language="fr-FR" + ) + ) + ) + config_dict = translation_config.model_dump() + self.assertEqual(config_dict["input"]["config"]["source_language"], "en-US") + self.assertEqual(config_dict["input"]["config"]["target_language"], "de-DE") + self.assertEqual(config_dict["input"]["type"], "sap_document_translation") + self.assertEqual(config_dict["output"]["config"]["source_language"], "de-DE") + self.assertEqual(config_dict["output"]["config"]["target_language"], "fr-FR") + self.assertEqual(config_dict["output"]["type"], "sap_document_translation") \ No newline at end of file diff --git a/packages/gen/tests/prompt_registry/__init__.py b/packages/gen/tests/prompt_registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/prompt_registry/test_flat_import_prompt_registry.py b/packages/gen/tests/prompt_registry/test_flat_import_prompt_registry.py new file mode 100644 index 0000000..dec661e --- /dev/null +++ b/packages/gen/tests/prompt_registry/test_flat_import_prompt_registry.py @@ -0,0 +1,21 @@ +def test_not_flat_and_flat_import_by_name(): + from gen_ai_hub.prompt_registry import PromptTemplateClient as client_flat + from gen_ai_hub.prompt_registry.client import PromptTemplateClient as client + assert client_flat == client + + from gen_ai_hub.prompt_registry import PromptTemplateClient as client_flat_2 + from gen_ai_hub.prompt_registry.client import PromptTemplateClient as client_2 + assert client_flat_2 == client_2 + + from gen_ai_hub.prompt_registry import PromptTemplate as prompt_flat + from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplate as prompt + assert prompt_flat == prompt + + from gen_ai_hub.prompt_registry import PromptTemplateSpec as prompt_spec_flat + from gen_ai_hub.prompt_registry.models.prompt_template import PromptTemplateSpec as prompt_spec + assert prompt_spec_flat == prompt_spec + + from gen_ai_hub.prompt_registry import OrchestrationConfigGetResponse as orchestration_config_get_response_flat + from gen_ai_hub.prompt_registry.models.orchestration_config import (OrchestrationConfigGetResponse + as orchestration_config_get_response) + assert orchestration_config_get_response_flat == orchestration_config_get_response \ No newline at end of file diff --git a/packages/gen/tests/prompt_registry/test_pr_client.py b/packages/gen/tests/prompt_registry/test_pr_client.py new file mode 100644 index 0000000..aba1b41 --- /dev/null +++ b/packages/gen/tests/prompt_registry/test_pr_client.py @@ -0,0 +1,352 @@ +import unittest +from unittest.mock import patch + +from ai_api_client_sdk.exception import AIAPIServerException + +from gen_ai_hub.prompt_registry.client import (PromptTemplateClient, OrchestrationConfigClient, + PATH_SCENARIOS, PATH_PROMPT_TEMPLATES, CONTENT_TYPE_JSON_, + PATH_REGISTRY_CONFIG, PATH_REGISTRY_SCENARIOS, ) +from gen_ai_hub.prompt_registry.models.prompt_template import (PromptTemplateSpec, PromptTemplate, + PromptTemplateSubstitutionResponse) +from tests.mock import (TEMPLATE_NAME, TEMPLATE_ID, VERSION, SCENARIO, TEMPLATE_YAML, ORCHESTRATION_CONFIG_NAME, + ORCHESTRATION_CONFIG_ID, TEMPLATE_POST_RESPONSE, TEMPLATE_LIST_RESPONSE, TEMPLATE_GET_RESPONSE, + TEMPLATE_DELETE_RESPONSE, TEMPLATE_SUBSTITUTION_REQUEST, TEMPLATE_SUBSTITUTION_RESPONSE, + ORCHESTRATION_CONFIG_POST_RESPONSE, ORCHESTRATION_CONFIG_LIST_RESPONSE, + ORCHESTRATION_CONFIG_GET_RESPONSE, ORCHESTRATION_CONFIG_DELETE_RESPONSE, + ORCHESTRATION_CONFIG_YAML, ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC, + get_mocked_ai_core_client) +from gen_ai_hub.prompt_registry.models.orchestration_config import (OrchestrationConfigDeleteResponse, + OrchestrationConfigGetResponse, + OrchestrationConfigPostResponse, + OrchestrationConfigListResponse) +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import UserMessage, TextPart, ImagePart +from gen_ai_hub.orchestration_v2.models.template import Template, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.response_format import ResponseFormatJsonObject +from gen_ai_hub.orchestration_v2.models.tools import FunctionTool +from gen_ai_hub.orchestration_v2.models.multimodal_items import ImageItem, ImageUrl + + +class TestPromptTemplateModelContent(unittest.TestCase): + + def test_text_part(self): + prompt_template_obj = PromptTemplate(role='system', content=['Hello, world!']) + self.assertIsInstance(prompt_template_obj.content[0], TextPart) + self.assertEqual(prompt_template_obj.content[0].text, 'Hello, world!') + + def test_image_part(self): + prompt_template_obj = PromptTemplate(role='system', content=[ImageItem(url="https://example.com/image.png")]) + self.assertIsInstance(prompt_template_obj.content[0], ImagePart) + self.assertIsInstance(prompt_template_obj.content[0].image_url, ImageUrl) + + def test_text_and_image_part(self): + prompt_template_obj = PromptTemplate(role='system', content=['Hello, world!', ImageItem(url="https://example.com/image.png")]) + self.assertIsInstance(prompt_template_obj.content[0], TextPart) + self.assertIsInstance(prompt_template_obj.content[1], ImagePart) + self.assertIsInstance(prompt_template_obj.content[1].image_url, ImageUrl) + self.assertEqual(prompt_template_obj.content[0].text, 'Hello, world!') + self.assertEqual(prompt_template_obj.content[1].image_url.url, 'https://example.com/image.png') + + def test_invalid_content(self): + with self.assertRaises(ValueError): + PromptTemplate(role='system', content=123) + + def test_invalid_content_list(self): + with self.assertRaises(ValueError): + PromptTemplate(role='system', content=[123, 'Hello, world!']) + + +class TestPromptTemplateClient(unittest.TestCase): + + def setUp(self): + proxy_client = get_mocked_ai_core_client(client_id='test') + self.test_client = PromptTemplateClient(proxy_client=proxy_client) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_prompt_template(self, mock_post): + spec = PromptTemplateSpec(template=[PromptTemplate(role='system', content='Hello, world!')]) + mock_post.return_value = TEMPLATE_POST_RESPONSE.model_dump() + + response = self.test_client.create_prompt_template(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION, + prompt_template_spec=spec) + + self.assertEqual(response, TEMPLATE_POST_RESPONSE) + mock_post.assert_called_once_with(path=PATH_PROMPT_TEMPLATES, + body={ 'name': TEMPLATE_NAME, 'version': VERSION, 'scenario': SCENARIO, + 'spec': spec.model_dump(by_alias=True, exclude_none=True)}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_prompt_template_with_response_format(self, mock_post): + spec = PromptTemplateSpec(template=[PromptTemplate(role='system', content='Hello, world!'), + PromptTemplate(role='user', + content='What is your name? Answer in JSON format.')], + response_format=ResponseFormatJsonObject()) + mock_post.return_value = TEMPLATE_POST_RESPONSE.model_dump() + + response = self.test_client.create_prompt_template(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION, + prompt_template_spec=spec) + + self.assertEqual(response, TEMPLATE_POST_RESPONSE) + mock_post.assert_called_once_with(path=PATH_PROMPT_TEMPLATES, + body={'name': TEMPLATE_NAME, 'version': VERSION, 'scenario': SCENARIO, + 'spec': spec.model_dump(by_alias=True, exclude_none=True)}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_prompt_template_with_tool(self, mock_post): + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + tool = FunctionTool.from_function(add) + spec = PromptTemplateSpec(template=[PromptTemplate(role="user", content='3 + 6')], + tools=[tool]) + mock_post.return_value = TEMPLATE_POST_RESPONSE.model_dump() + + response = self.test_client.create_prompt_template(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION, + prompt_template_spec=spec) + + self.assertEqual(response, TEMPLATE_POST_RESPONSE) + mock_post.assert_called_once_with(path=PATH_PROMPT_TEMPLATES, + body={'name': TEMPLATE_NAME, 'version': VERSION, 'scenario': SCENARIO, + 'spec': spec.model_dump(by_alias=True, exclude_none=True)}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_prompt_template_with_image_input(self, mock_post): + spec = PromptTemplateSpec(template=[PromptTemplate(role="user", content='Whai is in the image?'), + PromptTemplate(role="user", + content=[ImageItem(url="https://example.com/image.png")])]) + mock_post.return_value = TEMPLATE_POST_RESPONSE.model_dump() + + response = self.test_client.create_prompt_template(scenario=SCENARIO, name=TEMPLATE_NAME, version=VERSION, + prompt_template_spec=spec) + + self.assertEqual(response, TEMPLATE_POST_RESPONSE) + mock_post.assert_called_once_with(path=PATH_PROMPT_TEMPLATES, + body={'name': TEMPLATE_NAME, 'version': VERSION, 'scenario': SCENARIO, + 'spec': spec.model_dump(by_alias=True, exclude_none=True)}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_prompt_templates(self, mock_get): + query_params = {'scenario': SCENARIO, 'name': TEMPLATE_ID, 'version': VERSION, + 'retrieve': None, 'include_spec': None} + mock_get.return_value = TEMPLATE_LIST_RESPONSE.model_dump() + + response = self.test_client.get_prompt_templates(query_params['scenario'], query_params['name'], + query_params['version']) + self.assertEqual(response, TEMPLATE_LIST_RESPONSE) + mock_get.assert_called_once_with(path=PATH_PROMPT_TEMPLATES, params=query_params) + + @patch('ai_api_client_sdk.helpers.rest_client.requests.Session') + def test_get_prompt_templates_error(self, mock_handle_request_session): + mock_handle_request_session.raise_for_status.side_effect = ( + AIAPIServerException(description='Error', error_message='Resource not found', status_code=404)) + + with self.assertRaises(AIAPIServerException): + self.test_client.get_prompt_templates(scenario=SCENARIO, name='XXX', version=VERSION) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_prompt_template_by_id(self, mock_get): + mock_get.return_value = TEMPLATE_GET_RESPONSE.model_dump() + + response = self.test_client.get_prompt_template_by_id(TEMPLATE_ID) + self.assertEqual(response, TEMPLATE_GET_RESPONSE) + mock_get.assert_called_once_with(path=f'{PATH_PROMPT_TEMPLATES}/{TEMPLATE_ID}') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_prompt_template_history(self, mock_get): + mock_get.return_value = TEMPLATE_LIST_RESPONSE.model_dump() + + response = self.test_client.get_prompt_template_history(SCENARIO, TEMPLATE_NAME, VERSION) + self.assertEqual(response, TEMPLATE_LIST_RESPONSE) + mock_get.assert_called_once_with(path=f'{PATH_SCENARIOS}/{SCENARIO}/promptTemplates/{TEMPLATE_NAME}/' + f'versions/{VERSION}/history') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.delete') + def test_delete_prompt_template_by_id(self, mock_delete): + mock_delete.return_value = TEMPLATE_DELETE_RESPONSE.model_dump() + + response = self.test_client.delete_prompt_template_by_id(TEMPLATE_ID) + self.assertEqual(response, TEMPLATE_DELETE_RESPONSE) + mock_delete.assert_called_once_with(path=f'{PATH_PROMPT_TEMPLATES}/{TEMPLATE_ID}') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_fill_prompt_template_by_id(self, mock_post): + mock_template = PromptTemplate(role='system', content='Hello, world!') + mock_response = PromptTemplateSubstitutionResponse(parsed_prompt=[mock_template], + resource=TEMPLATE_GET_RESPONSE) + mock_post.return_value = mock_response.model_dump() + + response = self.test_client.fill_prompt_template_by_id(template_id=TEMPLATE_ID, + input_params=TEMPLATE_SUBSTITUTION_REQUEST.input_params, + metadata=True) + self.assertEqual(response, mock_response) + mock_post.assert_called_once_with( + path=f'{PATH_PROMPT_TEMPLATES}/{TEMPLATE_ID}/substitution', + headers={"Content-Type": CONTENT_TYPE_JSON_}, + body=TEMPLATE_SUBSTITUTION_REQUEST.model_dump(by_alias=True), + params={'metadata': True}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_fill_prompt_template(self, mock_post): + mock_post.return_value = TEMPLATE_SUBSTITUTION_RESPONSE.model_dump() + + response = self.test_client.fill_prompt_template(SCENARIO, TEMPLATE_NAME, VERSION, + TEMPLATE_SUBSTITUTION_REQUEST.input_params) + self.assertEqual(response, TEMPLATE_SUBSTITUTION_RESPONSE) + mock_post.assert_called_once_with( + path= f'{PATH_SCENARIOS}/{SCENARIO}/promptTemplates/{TEMPLATE_NAME}/versions/{VERSION}/substitution', + headers={"Content-Type": CONTENT_TYPE_JSON_}, + body=TEMPLATE_SUBSTITUTION_REQUEST.model_dump(by_alias=True), + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_import_prompt_template(self, mock_post): + mock_post.return_value = TEMPLATE_POST_RESPONSE.model_dump() + + binary_file_content = TEMPLATE_YAML.encode('utf-8') + response = self.test_client.import_prompt_template(binary_file_content) + self.assertEqual(response, TEMPLATE_POST_RESPONSE) + mock_post.assert_called_once_with(path=f'{PATH_PROMPT_TEMPLATES}/import', files={'file': binary_file_content}) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_export_prompt_template(self, mock_get): + mock_get.return_value = TEMPLATE_YAML.encode('utf-8') + + response = self.test_client.export_prompt_template(TEMPLATE_ID) + self.assertEqual(response, TEMPLATE_YAML.encode('utf-8')) + mock_get.assert_called_once_with(path=f'{PATH_PROMPT_TEMPLATES}/{TEMPLATE_ID}/export', + return_bytes_content=True) + +class TestOrchestrationConfigClient(unittest.TestCase): + + def setUp(self): + proxy_client = get_mocked_ai_core_client(client_id='test') + self.test_client = OrchestrationConfigClient(proxy_client=proxy_client) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_create_orchestration_config(self, mock_post): + spec = OrchestrationConfig( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=Template(template=[UserMessage(content="Hello, World!")]), + model=LLMModelDetails(name="gpt-4o-mini") + ) + ) + ) + mock_post.return_value = ORCHESTRATION_CONFIG_POST_RESPONSE + + response = self.test_client.create_orchestration_config(scenario=SCENARIO, name=ORCHESTRATION_CONFIG_NAME, version=VERSION, + spec=spec) + + self.assertIsInstance(response, OrchestrationConfigPostResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_POST_RESPONSE) + mock_post.assert_called_once_with(path=PATH_REGISTRY_CONFIG, + body={ 'name': ORCHESTRATION_CONFIG_NAME, 'version': VERSION, 'scenario': SCENARIO, + 'spec': spec.model_dump()}, + convert_body_to_camel_case=False + ) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_orchestration_configs(self, mock_get): + query_params = {'scenario': SCENARIO, 'name': ORCHESTRATION_CONFIG_NAME, 'version': VERSION, + 'retrieve': None, 'include_spec': None, 'resolve_template_ref': None} + mock_get.return_value = ORCHESTRATION_CONFIG_LIST_RESPONSE + + response = self.test_client.get_orchestration_configs(**query_params) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_LIST_RESPONSE) + mock_get.assert_called_once_with(path=PATH_REGISTRY_CONFIG, params=query_params, + convert_params_to_camel_case=False) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_orchestration_configs_with_spec(self, mock_get): + query_params = {'scenario': SCENARIO, 'name': ORCHESTRATION_CONFIG_NAME, 'version': VERSION, + 'retrieve': None, 'include_spec': True, 'resolve_template_ref': None} + mock_get.return_value = ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC + + response = self.test_client.get_orchestration_configs(**query_params) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC) + mock_get.assert_called_once_with(path=PATH_REGISTRY_CONFIG, params=query_params, + convert_params_to_camel_case=False) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_orchestration_config_by_id(self, mock_get): + mock_get.return_value = ORCHESTRATION_CONFIG_GET_RESPONSE + + response = self.test_client.get_orchestration_config_by_id(ORCHESTRATION_CONFIG_ID, resolve_template_ref=False) + self.assertIsInstance(response, OrchestrationConfigGetResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_GET_RESPONSE) + mock_get.assert_called_once_with(path=f'{PATH_REGISTRY_CONFIG}/{ORCHESTRATION_CONFIG_ID}', + params={'resolve_template_ref': False}, + convert_params_to_camel_case=False) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_orchestration_config_history(self, mock_get): + mock_get.return_value = ORCHESTRATION_CONFIG_LIST_RESPONSE + + response = self.test_client.get_orchestration_config_history(scenario=SCENARIO, name=ORCHESTRATION_CONFIG_NAME, + version=VERSION, include_spec=None, + resolve_template_ref=None, + ) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_LIST_RESPONSE) + mock_get.assert_called_once_with(path=f"{PATH_REGISTRY_SCENARIOS}/{SCENARIO}/orchestrationConfigs/" + f"{ORCHESTRATION_CONFIG_NAME}/versions/{VERSION}/history", + params={'include_spec': None, 'resolve_template_ref': None}, + convert_params_to_camel_case=False) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_get_orchestration_config_history_with_spec(self, mock_get): + mock_get.return_value = ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC + + response = self.test_client.get_orchestration_config_history(scenario=SCENARIO, name=ORCHESTRATION_CONFIG_NAME, + version=VERSION, include_spec=True, + resolve_template_ref=None, + ) + self.assertIsInstance(response, OrchestrationConfigListResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_LIST_RESPONSE_WITH_SPEC) + mock_get.assert_called_once_with(path=f"{PATH_REGISTRY_SCENARIOS}/{SCENARIO}/orchestrationConfigs/" + f"{ORCHESTRATION_CONFIG_NAME}/versions/{VERSION}/history", + params={'include_spec': True, 'resolve_template_ref': None}, + convert_params_to_camel_case=False) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.delete') + def test_delete_orchestration_config_by_id(self, mock_delete): + mock_delete.return_value = ORCHESTRATION_CONFIG_DELETE_RESPONSE + + response = self.test_client.delete_orchestration_config_by_id(ORCHESTRATION_CONFIG_ID) + self.assertIsInstance(response, OrchestrationConfigDeleteResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_DELETE_RESPONSE) + mock_delete.assert_called_once_with(path=f'{PATH_REGISTRY_CONFIG}/{ORCHESTRATION_CONFIG_ID}') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.post') + def test_import_orchestration_config(self, mock_post): + mock_post.return_value = ORCHESTRATION_CONFIG_POST_RESPONSE + + binary_file_content = ORCHESTRATION_CONFIG_YAML.encode('utf-8') + response = self.test_client.import_orchestration_config(binary_file_content) + self.assertIsInstance(response, OrchestrationConfigPostResponse) + self.assertEqual(response.model_dump(), ORCHESTRATION_CONFIG_POST_RESPONSE) + mock_post.assert_called_once_with(path=f'{PATH_REGISTRY_CONFIG}/import', files={'file': binary_file_content}) + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_export_orchestration_config(self, mock_get): + mock_get.return_value = ORCHESTRATION_CONFIG_YAML.encode('utf-8') + + response = self.test_client.export_orchestration_config(ORCHESTRATION_CONFIG_ID) + self.assertEqual(response, ORCHESTRATION_CONFIG_YAML.encode('utf-8')) + mock_get.assert_called_once_with(path=f'{PATH_REGISTRY_CONFIG}/{ORCHESTRATION_CONFIG_ID}/export', + return_bytes_content=True) \ No newline at end of file diff --git a/packages/gen/tests/proxy/__init__.py b/packages/gen/tests/proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/proxy/core/__init__.py b/packages/gen/tests/proxy/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/proxy/core/test_base.py b/packages/gen/tests/proxy/core/test_base.py new file mode 100644 index 0000000..af42e8c --- /dev/null +++ b/packages/gen/tests/proxy/core/test_base.py @@ -0,0 +1,68 @@ +import os +import pathlib +import unittest +from unittest import TestCase +from unittest.mock import patch + +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context +from tests.mock import MockDeployment, MockProxyClient + + +class TestBaseClasses(TestCase): + + def test_instance_cache(self): + with proxy_version_context('mock'): + client_a = get_proxy_client() + self.assertIsInstance(client_a, MockProxyClient) + client_b = get_proxy_client() + self.assertIs(client_a, client_b) + MockProxyClient.refresh_instance_cache() + client_c = get_proxy_client() + self.assertIsNot(client_a, client_c) + + def test_base_class_functions(self): + proxy_client = MockProxyClient() + self.assertEqual(proxy_client.deployment_class, MockDeployment) + self.assertDictEqual(proxy_client.request_header, {'token': 'mock_token'}) + self.assertEqual(len(proxy_client.deployments), 1) + self.assertTrue(all([isinstance(deployment, MockDeployment) for deployment in proxy_client.deployments])) + deployment = proxy_client.select_deployment() + self.assertIsInstance(deployment, MockDeployment) + self.assertEqual(deployment.url, 'mock_url') + self.assertEqual(deployment.prediction_url, 'mock_url/predict') + self.assertEqual(deployment.get_main_model_identification_kwargs(), 'a') + self.assertEqual(deployment.get_model_identification_kwargs(), ('a', 'b', 'c')) + + +class TestBaseClasses(TestCase): + + def test_instance_cache(self): + with proxy_version_context('mock'): + client_a = get_proxy_client() + self.assertIsInstance(client_a, MockProxyClient) + client_b = get_proxy_client() + self.assertIs(client_a, client_b) + MockProxyClient.refresh_instance_cache() + client_c = get_proxy_client() + self.assertIsNot(client_a, client_c) + + def test_base_class_functions(self): + proxy_client = MockProxyClient() + self.assertEqual(proxy_client.deployment_class, MockDeployment) + self.assertDictEqual(proxy_client.request_header, {'token': 'mock_token'}) + self.assertEqual(len(proxy_client.deployments), 1) + self.assertTrue(all([isinstance(deployment, MockDeployment) for deployment in proxy_client.deployments])) + deployment = proxy_client.select_deployment() + self.assertIsInstance(deployment, MockDeployment) + self.assertEqual(deployment.url, 'mock_url') + self.assertEqual(deployment.prediction_url, 'mock_url/predict') + self.assertEqual(deployment.get_main_model_identification_kwargs(), 'a') + self.assertEqual(deployment.get_model_identification_kwargs(), ('a', 'b', 'c')) + + self.assertEqual(proxy_client.get_home(), pathlib.Path('~/.mock_llm').expanduser()) + with patch.dict(os.environ, {'MOCK_LLM_HOME': '~/mock_home'}): + self.assertEqual(proxy_client.get_home(), pathlib.Path('~/mock_home').expanduser()) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/proxy/core/test_proxy_clients.py b/packages/gen/tests/proxy/core/test_proxy_clients.py new file mode 100644 index 0000000..9d87041 --- /dev/null +++ b/packages/gen/tests/proxy/core/test_proxy_clients.py @@ -0,0 +1,77 @@ +import unittest + +from gen_ai_hub.proxy.core.proxy_clients import ( + ProxyClients, + get_proxy_client, + get_proxy_version, + proxy_version_context, + set_proxy_version, +) +from tests.mock import MockProxyClient + + +class TestProxyManagement(unittest.TestCase): + + def test_proxy_version_context(self): + """ Test the proxy_version_context context manager. """ + original_value = get_proxy_version() + new_value = original_value + '_new' + with proxy_version_context(new_value): + self.assertEqual(get_proxy_version(), new_value) + # Test restoring previous state + self.assertEqual(get_proxy_version(), original_value) + + def test_set_proxy_version(self): + """ Test setting the proxy version. """ + catalog = ProxyClients() + set_proxy_version('v2', catalog=catalog) + self.assertEqual(get_proxy_version(catalog=catalog), 'v2') + + def test_get_proxy_client(self): + """ Test getting a proxy client. """ + proxy_clients = ProxyClients() + + @proxy_clients.register('test') + class TestProxyClient(MockProxyClient): + pass + + client = get_proxy_client('test', catalog=proxy_clients) + self.assertIsInstance(client, TestProxyClient) + + def test_proxy_clients_registration(self): + """ Test registering and retrieving a proxy client class. """ + proxy_clients = ProxyClients() + proxy_clients.register('mock')(MockProxyClient) + + @proxy_clients.register('mock_v2') + class DifferentProxyClient(MockProxyClient): + pass + + self.assertIs(proxy_clients.get_proxy_cls('mock_v2'), DifferentProxyClient) + + def test_get_proxy_cls_name(self): + """ Test retrieving the name of a registered proxy client class. """ + proxy_clients = ProxyClients() + + proxy_clients.register('mock')(MockProxyClient) + + self.assertEqual(proxy_clients.get_proxy_cls_name(MockProxyClient), 'mock') + + def test_errors(self): + """ Test error handling in various functions. """ + proxy_clients = ProxyClients() + + with self.assertRaises(ValueError): + set_proxy_version(123) # Not a string + + with self.assertRaises(ValueError): + @proxy_clients.register('test') + class NotAProxyClient: + pass + + with self.assertRaises(ValueError): + proxy_clients.get_proxy_cls_name(str) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/proxy/core/test_utils.py b/packages/gen/tests/proxy/core/test_utils.py new file mode 100644 index 0000000..ad6de8e --- /dev/null +++ b/packages/gen/tests/proxy/core/test_utils.py @@ -0,0 +1,115 @@ +import time +import unittest + +from gen_ai_hub.proxy.core.utils import lru_cache_extended + + +class TestLRUCacheDecorators(unittest.TestCase): + + def test_lru_cache_clear(self): + @lru_cache_extended() + def test_func(x): + test_func.counter += 1 + return x * x + + test_func.counter = 0 + + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + test_func.cache_clear() + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 2) + + def test_lru_cache_extended_first_arg_self(self): + class NotHashable: + __hash__ = None + + def __init__(self, x): + self.counter = 0 + self.x = x + + @lru_cache_extended(first_arg_self=True) + def test_func(self, y): + self.counter += 1 + return self.x * y + + @property + @lru_cache_extended(first_arg_self=True) + def x_prop(self): + self.counter += 1 + return self.x + + obj = NotHashable(2) + self.assertEqual(obj.test_func(2), 4) + self.assertEqual(obj.counter, 1) + self.assertEqual(obj.test_func(2), 4) + self.assertEqual(obj.counter, 1) + + obj = NotHashable(3) + self.assertEqual(obj.test_func(2), 6) + self.assertEqual(obj.counter, 1) + self.assertEqual(obj.test_func(2), 6) + self.assertEqual(obj.counter, 1) + + x = obj.x_prop + self.assertEqual(x, obj.x) + self.assertEqual(obj.counter, 2) + x = obj.x_prop + self.assertEqual(obj.counter, 2) + + def test_lru_cache_extended_refresh(self): + @lru_cache_extended(maxsize=2) + def test_func(x): + test_func.counter += 1 + return x * x + + test_func.counter = 0 + + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + self.assertEqual(test_func(3), 9) + self.assertEqual(test_func.counter, 2) + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 2) + + # Test cache clearing + test_func(2, _recache=True) + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 3) + + def test_lru_cache_extended_timeout(self): + @lru_cache_extended(timeout=1, maxsize=2) + def test_func(x): + test_func.counter += 1 + return x * x + + test_func.counter = 0 + + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + time.sleep(1.1) # Wait for the cache to expire + self.assertEqual(test_func(2), 4) # Should recompute + self.assertEqual(test_func.counter, 2) + + def test_lru_cache_extended_typed(self): + @lru_cache_extended(typed=True) + def test_func(x): + test_func.counter += 1 + return x * x + + test_func.counter = 0 + + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 1) + self.assertEqual(test_func(2.0), 4) + self.assertEqual(test_func.counter, 2) + self.assertEqual(test_func(2), 4) + self.assertEqual(test_func.counter, 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/packages/gen/tests/proxy/gen_ai_hub_proxy/__init__.py b/packages/gen/tests/proxy/gen_ai_hub_proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py new file mode 100644 index 0000000..69603c6 --- /dev/null +++ b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_additional_header_e2e.py @@ -0,0 +1,100 @@ +import asyncio +import json +import threading +import unittest +from collections import Counter +from contextlib import contextmanager + +import respx +from httpx import Response + +from gen_ai_hub.proxy.gen_ai_hub_proxy import temporary_headers_addition +from gen_ai_hub.proxy.native.openai import AsyncOpenAI, OpenAI +from tests.mock import ( + get_mocked_ai_core_client, + OPENAI_EMBEDDINGS_RESPONSE +) + + +class AsyncOpenAITests(unittest.IsolatedAsyncioTestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + + async def test_async_embedding(self): + kwargs = {'model_name': 'gpt-4o-mini'} + client = AsyncOpenAI(proxy_client=self.proxy_client) + + counter = Counter() + + def mock_callback(request): + body = json.loads(request.content.decode('utf-8')) + self.assertEqual(body['input'], request.headers['test-func']) + counter[request.headers['test-func']] += 1 + return Response(200, json=OPENAI_EMBEDDINGS_RESPONSE) + + @contextmanager + def mocker(deployment_url): + with respx.mock: + route = respx.post(deployment_url).mock(side_effect=mock_callback) + yield route + + n_requests = 10 + + async def test_f(test_value): + with temporary_headers_addition({'test-func': test_value}): + for _ in range(n_requests): + await client.embeddings.create(**{**kwargs, 'input': test_value}) + + deployment = self.proxy_client.select_deployment(model_name='text-embedding-ada-002') + with mocker(deployment.prediction_url): + await asyncio.gather(*[test_f(str(i)) for i in range(5)]) + for key, value in counter.items(): + self.assertEqual(value, n_requests, f"Expected {key} but got {value}") + + +class SyncOpenAITests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.proxy_client = get_mocked_ai_core_client() + + def test_sync_embedding(self): + kwargs = {'model_name': 'gpt-4o-mini'} + client = OpenAI(proxy_client=self.proxy_client) + + counter = Counter() + + def mock_callback(request): + body = json.loads(request.content.decode('utf-8')) + self.assertEqual(body['input'], request.headers['test-func']) + counter[request.headers['test-func']] += 1 + return Response(200, json=OPENAI_EMBEDDINGS_RESPONSE) + + @contextmanager + def mocker(deployment_url): + with respx.mock: + route = respx.post(deployment_url).mock(side_effect=mock_callback) + yield route + + n_requests = 10 + + def test_f(test_value): + with temporary_headers_addition({'test-func': test_value}): + for _ in range(n_requests): + client.embeddings.create(**{**kwargs, 'input': test_value}) + + deployment = self.proxy_client.select_deployment(model_name='text-embedding-ada-002') + with mocker(deployment.prediction_url): + threads = [ + threading.Thread(target=test_f, args=(str(i),)) + for i in range(5) + ] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + for key, value in counter.items(): + self.assertEqual(value, n_requests, f"Expected {key} but got {value}") diff --git a/packages/gen/tests/proxy/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py new file mode 100644 index 0000000..e518b86 --- /dev/null +++ b/packages/gen/tests/proxy/gen_ai_hub_proxy/test_gen_ai_hub_proxy.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import os +import threading +import unittest +from datetime import datetime +from unittest.mock import MagicMock + +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import (GenAIHubProxyClient, Deployment, temporary_headers_addition) +from tests.mock import get_mocked_ai_core_client, ai_core_ai_api_mocker + + +class TestProxyClient(unittest.TestCase): + proxy_kwargs = dict( + client_id='XXXX', + client_secret='YYYY', + auth_url='https://auth_url/oauth/token', + base_url='https://base_url/v2', + ) + + def create_deployment(self): + return Deployment( + url='url', + deployment_id='deployment_id', + config_name='config_name', + config_id='config_id', + model_name='gpt-4o-mini', + model_version='1337', + additonal_parameters={}, + created_at=datetime.now(), + ) + + def test_get_proxy_client(self): + with proxy_version_context('gen-ai-hub'): + client_a = get_proxy_client(**self.proxy_kwargs) + self.assertIsInstance(client_a, GenAIHubProxyClient) + client_b = get_proxy_client(**self.proxy_kwargs) + self.assertIsInstance(client_b, GenAIHubProxyClient) + self.assertEqual(client_b, client_a) + + def test_deployment(self): + deployment = self.create_deployment() + self.assertEqual(deployment.model_version, '1337') + self.assertEqual(deployment.model_name, 'gpt-4o-mini') + self.assertTupleEqual(deployment.get_model_identification_kwargs(), + ('model_name', 'model_version', 'config_id', 'config_name', 'deployment_id')) + self.assertEqual(deployment.prediction_url, None) + with self.assertRaises(AttributeError): + deployment.not_existing + self.assertEqual(deployment.model_version, '1337') + + def test_deployment_discovery(self): + with (proxy_version_context('gen-ai-hub')): + proxy_client = get_proxy_client(**self.proxy_kwargs) + deployment = self.create_deployment() + proxy_client._deployments = [deployment] + deployments_dict = {deployment.deployment_id: deployment} + proxy_client._get_scenario_deployments = MagicMock() + proxy_client._get_scenario_deployments.return_value = deployments_dict + self.assertIsInstance(proxy_client, GenAIHubProxyClient) + for kwarg in ('deployment_id', 'config_name', 'config_id', 'model_name', 'not_existing'): + kwargs = {kwarg: 'NOT_EXISTING'} + with self.assertRaises(ValueError): + proxy_client.select_deployment(**kwargs) + proxy_client._get_scenario_deployments.assert_called_once_with( + proxy_client.foundational_model_scenarios[0]) + proxy_client._get_scenario_deployments.reset_mock() + deployment = proxy_client.select_deployment(model_name='gpt-4o-mini') + self.assertIs(deployment, proxy_client.deployments[0]) + for kwarg in ('deployment_id', 'config_name', 'config_id', 'model_name'): + value = getattr(deployment, kwarg) + deployment_other = proxy_client.select_deployment(**{kwarg: value}) + self.assertIs(deployment, deployment_other) + search_kwargs = {kwarg: value, 'model_version': '1337'} + if kwarg != "model_name": + search_kwargs['model_name'] = getattr(deployment, 'model_name') + deployment_other = proxy_client.select_deployment(**search_kwargs) + self.assertIs(deployment, deployment_other) + with self.assertRaises(ValueError): + proxy_client.select_deployment(**{kwarg: value, 'model_version': '1340'}) + + def test_deployment_discovery_error_model_version_without_model_name(self): + with (proxy_version_context('gen-ai-hub')): + proxy_client = get_proxy_client(**self.proxy_kwargs) + deployment = self.create_deployment() + proxy_client._deployments = [deployment] + deployments_dict = {deployment.deployment_id: deployment} + proxy_client._get_scenario_deployments = MagicMock() + proxy_client._get_scenario_deployments.return_value = deployments_dict + self.assertIsInstance(proxy_client, GenAIHubProxyClient) + with self.assertRaises(ValueError): + proxy_client.select_deployment(model_version = '1337') + + def test_client_mock_discovery(self): + proxy_client = get_mocked_ai_core_client() + self.assertEqual(len(proxy_client.get_deployments()), 8) + self.assertIsInstance(proxy_client.request_header, dict) + + def test_client_mock_discovery_with_additional_fm_scenario(self): + from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubProxyClient + from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context + + with proxy_version_context('gen-ai-hub'): + kwargs = dict( + client_id='XXXX', + client_secret='YYYY', + auth_url='https://auth_url/oauth/token', + base_url='https://base_url/v2', + ) + with unittest.mock.patch.object(GenAIHubProxyClient, 'foundational_model_scenarios', []): + proxy_client: GenAIHubProxyClient = get_proxy_client(**kwargs) + with ai_core_ai_api_mocker(auth_url=kwargs['auth_url'], base_url=kwargs['base_url']): + proxy_client.get_request_header() + self.assertEqual(len(proxy_client.get_deployments()), 0) + proxy_client.add_foundation_model_scenario( + scenario_id='foundation-models', + config_names='not-existing-config-1', + ) + self.assertEqual(len(proxy_client.get_deployments()), 0) + proxy_client.add_foundation_model_scenario( + scenario_id='foundation-models', + config_names=['not-existing-config-2', 'not-existing-config-3'], + ) + self.assertEqual(len(proxy_client.get_deployments()), 0) + proxy_client.add_foundation_model_scenario( + scenario_id='foundation-models', + config_names='*', + ) + self.assertEqual(len(proxy_client.get_deployments()), 8) + proxy_client.add_foundation_model_scenario( + scenario_id='dox-llm', + config_names='dox-llm-cinder?lla*', + ) + self.assertEqual(len(proxy_client.get_deployments()), 9) + + def test_ai_client_type_headers(self): + test_client = 'Custom Test Client' + + with unittest.mock.patch.object(GenAIHubProxyClient, 'AI_CLIENT_TYPE_VAL', test_client): + GenAIHubProxyClient.clear_cache() + proxy_client = get_mocked_ai_core_client() + headers = proxy_client.request_header + self.assertIn('AI-Client-Type', headers) + self.assertEqual(test_client, headers['AI-Client-Type']) + GenAIHubProxyClient.clear_cache() + + def test_setting_headers_addition(self): + proxy_client = get_mocked_ai_core_client() + default_headers = proxy_client.request_header + + proxy_client.set_headers_addition({'X-Test-Header': 'default'}) + + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'default' + }) + + def test_updating_headers_addition(self): + proxy_client = get_mocked_ai_core_client() + headers = proxy_client.request_header + + self.assertIn('AI-Client-Type', headers, ) + self.assertEqual('GenAI Hub SDK (Python)', headers['AI-Client-Type']) + + def test_when_token_generation_is_skipped(self): + os.environ['SKIP_AUTHORIZATION'] = 'true' + mock_client = MagicMock() + mock_client.ai_core_client.rest_client.headers = {"Existing": "Header"} + mock_client.get_ai_core_token.return_value = "token123" + result = GenAIHubProxyClient.get_request_header(mock_client) + self.assertNotIn("Authorization", result) + del os.environ['SKIP_AUTHORIZATION'] + + def test_when_token_is_generated(self): + mock_client = MagicMock() + mock_client.ai_core_client.rest_client.headers = {"Existing": "Header"} + mock_client.get_ai_core_token.return_value = "token123" + result = GenAIHubProxyClient.get_request_header(mock_client) + self.assertIn("Authorization", result) + self.assertEqual(result["Authorization"], "token123") + + def test_setting_temporary_headers(self): + proxy_client = get_mocked_ai_core_client() + default_headers = proxy_client.request_header + + proxy_client.set_headers_addition({ + 'X-Test-Header': 'default' + }) + + with temporary_headers_addition({ + 'X-Test-Header': 'override', 'X-New-Header': 'new' + }): + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'override', + 'X-New-Header': 'new' + }) + + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'default' + }) + + def test_nesting_temporary_headers(self): + proxy_client = get_mocked_ai_core_client() + default_headers = proxy_client.request_header + + proxy_client.set_headers_addition({ + 'X-Test-Header': 'default' + }) + + with temporary_headers_addition({ + 'X-Test-Header': 'override', 'X-New-Header': 'new' + }): + with temporary_headers_addition({ + 'X-Test-Header': 'override-2', 'X-Another-Header': 'another' + }): + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'override-2', + 'X-Another-Header': 'another' + }) + + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'override', + 'X-New-Header': 'new' + }) + + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'default' + }) + + def test_setting_temporary_headers_multi_threaded(self): + proxy_client = get_mocked_ai_core_client() + default_headers = proxy_client.request_header + + proxy_client.set_headers_addition({ + 'X-Test-Header': 'default' + }) + + def task1(): + with temporary_headers_addition({ + 'X-Test-Header': 'override_thread1', + 'X-New-Header': 'new_thread1' + }): + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'override_thread1', + 'X-New-Header': 'new_thread1' + }) + + def task2(): + with temporary_headers_addition({ + 'X-Test-Header': 'override_thread2', + 'X-New-Header': 'new_thread2' + }): + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'override_thread2', + 'X-New-Header': 'new_thread2' + }) + + threads = [] + + for _ in range(10): + threads.append(threading.Thread(target=task1)) + threads.append(threading.Thread(target=task2)) + + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + self.assertEqual(proxy_client.request_header, { + **default_headers, + 'X-Test-Header': 'default' + }) diff --git a/packages/gen/tests/proxy/langchain_/__init__.py b/packages/gen/tests/proxy/langchain_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/gen/tests/proxy/langchain_/test_amazon.py b/packages/gen/tests/proxy/langchain_/test_amazon.py new file mode 100644 index 0000000..c3eac9d --- /dev/null +++ b/packages/gen/tests/proxy/langchain_/test_amazon.py @@ -0,0 +1,130 @@ +import unittest +from unittest.mock import patch + +from botocore.config import Config +from langchain_classic.chains import LLMChain +from langchain_classic.prompts import ( + AIMessagePromptTemplate, + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate, +) + +from gen_ai_hub.proxy.langchain.amazon import BedrockEmbeddings, ChatBedrock, ChatBedrockConverse, init_chat_model +from tests.mock import ( + AMAZON_TITAN_EMBED_QUERY_RESPONSE, + AMAZON_BEDROCK_INVOKE_RESPONSE, + get_mocked_ai_core_client, +) + + +class TestAmazonLangchain(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + + def test_model_kwargs(self): + model_kwargs = {"top_k": 3, "stop_sequences": ["abc"]} + chat_model = init_chat_model(self.proxy_client, + deployment=self.proxy_client.select_deployment( + model_name='amazon--nova-premier'), + **model_kwargs) + self.assertIsInstance(chat_model, ChatBedrock) + + @patch("langchain_classic.chains.base.Chain.invoke") + def test_chat_model(self, mock_chain_invoke): + mock_chain_invoke.return_value = AMAZON_BEDROCK_INVOKE_RESPONSE + chat_model = ChatBedrock( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + + template = "You are a helpful assistant that translates english to pirate." + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template("Hi") + example_ai = AIMessagePromptTemplate.from_template("Ahoy!") + human_template = "{text}" + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke("I love programming") + self.assertIsInstance(response["text"], str) + + @patch("langchain_community.embeddings.bedrock.BedrockEmbeddings.embed_query") + def test_embedding_model(self, mock_chain_invoke): + mock_chain_invoke.return_value = AMAZON_TITAN_EMBED_QUERY_RESPONSE + embedding_model = BedrockEmbeddings( + model_name="amazon--titan-embed-text", proxy_client=self.proxy_client + ) + response = embedding_model.embed_query("Your text string goes here") + self.assertIsInstance(response["embedding"], list) + self.assertTrue(all(isinstance(item, float) for item in response["embedding"])) + + def test_no_model_id(self): + with self.assertRaises(ValueError): + BedrockEmbeddings(proxy_client=self.proxy_client) + with self.assertRaises(ValueError): + ChatBedrock(proxy_client=self.proxy_client) + with self.assertRaises(ValueError): + ChatBedrockConverse(proxy_client=self.proxy_client) + + def test_no_model_name(self): + with self.assertRaises(ValueError): + ChatBedrock.get_corresponding_model_id(full_model_name="") + + def test_model_id_not_empty(self): + with self.assertRaises(ValueError): + BedrockEmbeddings(model_id="abc", proxy_client=self.proxy_client) + with self.assertRaises(ValueError): + ChatBedrock(model_id="abc", proxy_client=self.proxy_client) + with self.assertRaises(ValueError): + ChatBedrockConverse(model_id="abc", proxy_client=self.proxy_client) + + @patch("langchain_classic.chains.base.Chain.invoke") + def test_chat_converse(self, mock_chain_invoke): + mock_chain_invoke.return_value = AMAZON_BEDROCK_INVOKE_RESPONSE + chat_model = ChatBedrockConverse( + model_name="amazon--nova-premier", proxy_client=self.proxy_client, + model="amazon--nova-premier" + ) + template = "You are a helpful assistant that translates english to pirate." + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template("Hi") + example_ai = AIMessagePromptTemplate.from_template("Ahoy!") + human_template = "{text}" + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke("I love programming") + self.assertIsInstance(response["text"], str) + + def test_chat_model_config(self): + boto_config = Config( + connect_timeout=59, + read_timeout=299, + ) + + chat_model = ChatBedrockConverse( + model_name="amazon--nova-premier", proxy_client=self.proxy_client, + model="amazon--nova-premier", + config=boto_config + ) + self.assertTrue(chat_model.client.meta.config.read_timeout == 299, + "Read Timeout not applied") + + self.assertTrue(chat_model.client.meta.config.connect_timeout == 59, + "Connect Timeout not applied") + + + diff --git a/packages/gen/tests/proxy/langchain_/test_google_genai.py b/packages/gen/tests/proxy/langchain_/test_google_genai.py new file mode 100644 index 0000000..a6e807b --- /dev/null +++ b/packages/gen/tests/proxy/langchain_/test_google_genai.py @@ -0,0 +1,48 @@ +import unittest +from unittest.mock import patch + +from langchain_classic.chains import LLMChain +from langchain_classic.prompts.chat import ( + AIMessagePromptTemplate, + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate, +) + +from gen_ai_hub.proxy.langchain.google_genai import ChatGoogleGenerativeAI +from tests.mock import GOOGLE_GEMINI_INVOKE_RESPONSE, get_mocked_ai_core_client + + +class TestGoogleGenerativeAILangchain(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + + @patch("langchain_classic.chains.base.Chain.invoke") + def test_chat_model(self, mock_chain_invoke): + mock_chain_invoke.return_value = GOOGLE_GEMINI_INVOKE_RESPONSE + chat_model = ChatGoogleGenerativeAI( + proxy_model_name="gemini-2.0-flash", proxy_client=self.proxy_client + ) + + template = "You are a helpful assistant that translates english to pirate." + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template("Hi") + example_ai = AIMessagePromptTemplate.from_template("Ahoy!") + human_template = "{text}" + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt] + ) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke("I love programming") + self.assertIsInstance(response["text"], str) + + def test_model_id_not_empty(self): + with self.assertRaises(ValueError): + ChatGoogleGenerativeAI(model_id="abc") diff --git a/packages/gen/tests/proxy/langchain_/test_init_models.py b/packages/gen/tests/proxy/langchain_/test_init_models.py new file mode 100644 index 0000000..bf78687 --- /dev/null +++ b/packages/gen/tests/proxy/langchain_/test_init_models.py @@ -0,0 +1,151 @@ +import unittest +from datetime import datetime +from unittest.mock import MagicMock + +import pytest + +from gen_ai_hub.proxy.core.proxy_clients import get_proxy_client, proxy_version_context +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import Deployment +from gen_ai_hub.proxy.langchain import amazon, google_genai, openai +from gen_ai_hub.proxy.langchain.init_models import ( + ModelType, + init_embedding_model, + init_llm, +) + +EMBEDDING_TEST_MODELS_TO_MODEL_CLASS_MAP = { + "gemini-embedding": google_genai.GoogleGenerativeAIEmbeddings, + "text-embedding-3-small": openai.OpenAIEmbeddings, + "nvidia--llama-3.2-nv-embedqa-1b": openai.OpenAIEmbeddings +} + +LLM_TEST_MODELS_MODEL_CLASS_MAP = { + "amazon--nova-micro": amazon.ChatBedrock, + "anthropic--claude-4-sonnet": amazon.ChatBedrock, + "gemini-2.5-flash-lite": google_genai.ChatGoogleGenerativeAI, + "gpt-5": openai.ChatOpenAI, + "mistralai--mistral-small-instruct": openai.ChatOpenAI +} + +TEST_MODELS = list(EMBEDDING_TEST_MODELS_TO_MODEL_CLASS_MAP.keys()) + list(LLM_TEST_MODELS_MODEL_CLASS_MAP.keys()) + + +class TestInitModels(unittest.TestCase): + + @staticmethod + def create_deployment(model_name: str): + return Deployment( + url=f'https://api.ai.internalprod.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/{model_name}-url', + deployment_id=f'{model_name}-deployment_id', + config_name=f'{model_name}-config_name', + config_id=f'{model_name}-config_id', + model_name=model_name, + model_version='latest', + additonal_parameters={'model_type': str(ModelType.LLM)}, + created_at=datetime.now(), + ) + + @staticmethod + def create_gen_ai_hub_deployments(): + deployments = [] + for model in TEST_MODELS: + deployments.append(TestInitModels.create_deployment(model_name=model)) + return deployments + + @classmethod + def setUpClass(cls) -> None: + with proxy_version_context('gen-ai-hub'): + cls.proxy_client = get_proxy_client( + base_url='https://api.gen-ai.io', + auth_url='https://auth.gen-ai.io', + client_id='XXX', + client_secret='XXX', + resource_group='XXX' + ) + cls.proxy_client._deployments = cls.create_gen_ai_hub_deployments() + cls.proxy_client._get_scenario_deployments = MagicMock() + cls.llm = LLM_TEST_MODELS_MODEL_CLASS_MAP + cls.emb = EMBEDDING_TEST_MODELS_TO_MODEL_CLASS_MAP + + def setUp(self): + self.proxy_client._get_scenario_deployments.reset_mock() + + def test_init_embedding_model(self): + for model_name, model_class in self.emb.items(): + model = init_embedding_model(model_name, proxy_client=self.proxy_client) + self.assertIsInstance(model, model_class) + + def test_init_llm(self): + model_kwargs = {'top_k': 3} + for model_name, model_class in self.llm.items(): + initiated_model = init_llm(model_name, proxy_client=self.proxy_client, **model_kwargs) + self.assertIsInstance(initiated_model, model_class) + + def test_non_existing_model(self): + with pytest.raises(ValueError): + init_llm('michelangelo-1475', proxy_client=self.proxy_client) + with pytest.raises(ValueError): + init_embedding_model('michelangelo-1475', proxy_client=self.proxy_client) + + def test_custom_model(self): + model_name = 'test-custom-model' + init_func = google_genai.init_chat_model + model_class = google_genai.ChatGoogleGenerativeAI + custom_deployment = self.create_deployment(model_name) + self.proxy_client._deployments.append(custom_deployment) + model = init_llm(model_name, proxy_client=self.proxy_client, init_func=init_func) + self.assertIsInstance(model, model_class) + self.proxy_client._get_scenario_deployments.assert_not_called() + + def test_custom_amazon_model(self): + model_name = 'test-custom-amazon-model' + model_id = f'{model_name}-id' + init_func = amazon.init_chat_model + model_class = amazon.ChatBedrock + custom_deployment = self.create_deployment(model_name) + self.proxy_client._deployments.append(custom_deployment) + model = init_llm(model_name, model_id=model_id, proxy_client=self.proxy_client, init_func=init_func) + self.assertIsInstance(model, model_class) + self.proxy_client._get_scenario_deployments.assert_not_called() + + def test_custom_amazon_embedding_model(self): + model_name = 'test-custom-embed-model' + model_id = f'{model_name}-id' + init_func = amazon.init_embedding_model + model_class = amazon.BedrockEmbeddings + custom_deployment = self.create_deployment(model_name) + self.proxy_client._deployments.append(custom_deployment) + model = init_embedding_model(model_name, model_id=model_id, proxy_client=self.proxy_client, init_func=init_func) + self.assertIsInstance(model, model_class) + self.proxy_client._get_scenario_deployments.assert_not_called() + + def test_update_deployments_called_for_custom_model(self): + model_name = 'test-custom-model' + init_func = google_genai.init_chat_model + model_class = google_genai.ChatGoogleGenerativeAI + custom_deployment = self.create_deployment(model_name) + deployments = self.create_gen_ai_hub_deployments() + deployments.append(custom_deployment) + deployments_dict = {} + for i in range(len(deployments)): + deployments_dict[i] = deployments[i] + self.proxy_client._get_scenario_deployments.return_value = deployments_dict + model = init_llm(model_name, proxy_client=self.proxy_client, init_func=init_func) + self.assertIsInstance(model, model_class) + self.proxy_client._get_scenario_deployments.assert_called_once_with( + self.proxy_client.foundational_model_scenarios[0]) + + def test_custom_model_fails_if_deployment_does_not_exist(self): + model_name = 'not-existing-model' + init_func = google_genai.init_chat_model + model_class = google_genai.ChatGoogleGenerativeAI + deployments = self.create_gen_ai_hub_deployments() + deployments_dict = {} + for i in range(len(deployments)): + deployments_dict[i] = deployments[i] + self.proxy_client._get_scenario_deployments.return_value = deployments_dict + + with self.assertRaises(ValueError) as cm: + init_llm(model_name, proxy_client=self.proxy_client, init_func=init_func) + self.assertIn('No deployment found', cm.exception.args[0]) + self.assertIn(model_name, cm.exception.args[0]) diff --git a/packages/gen/tests/proxy/langchain_/test_openai.py b/packages/gen/tests/proxy/langchain_/test_openai.py new file mode 100644 index 0000000..be1a6e2 --- /dev/null +++ b/packages/gen/tests/proxy/langchain_/test_openai.py @@ -0,0 +1,222 @@ +import unittest + +from integration_tests.constants import NVIDIA_EMBEDDING_TEST_MODEL +from tests.mock import ( + get_mocked_ai_core_client, + openai_chat_completion_mocker, + openai_completion_mocker, + openai_embeddings_mocker, + cohere_chat_completion_mocker, +) + +try: + import openai as _ + + no_openai = False +except ImportError as err: + no_openai = True + +try: + from langchain_classic.chains import LLMChain + from langchain_classic.prompts.chat import ( + AIMessagePromptTemplate, + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate, + ) + + from gen_ai_hub.proxy.langchain.openai import ChatOpenAI, OpenAI, OpenAIEmbeddings + + no_langchain = False +except ImportError as err: + no_langchain = True + + +@unittest.skipIf(no_openai or no_langchain, 'langchain or openai not installed') +class TestOpenAILangchain(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + + def test_embedding_model(self): + deployment = self.proxy_client.select_deployment(model_name='text-embedding-ada-002') + + with openai_embeddings_mocker(deployment.prediction_url): + embedding_model = OpenAIEmbeddings(proxy_client=self.proxy_client, + proxy_model_name='text-embedding-ada-002') + self.assertIsNotNone(embedding_model.model) + response = embedding_model.embed_query('Your text string goes here') + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + + def test_nvidia_embedding(self, model=NVIDIA_EMBEDDING_TEST_MODEL): + deployment = self.proxy_client.select_deployment(model_name=model) + + with openai_embeddings_mocker(deployment.prediction_url): + # Test without input_type parameter + with self.assertRaises(ValueError) as cm: + embedding_model = OpenAIEmbeddings(proxy_client=self.proxy_client, + proxy_model_name=model) + embedding_model.embed_query('Your text string goes here') + + self.assertIn("input_type parameter is required", str(cm.exception)) + self.assertIn(model, str(cm.exception)) + + # Test with proper input_type parameter + embedding_model = OpenAIEmbeddings( + proxy_client=self.proxy_client, + proxy_model_name=model, + input_type='query' + ) + self.assertIsNotNone(embedding_model.model) + response = embedding_model.embed_query('Your text string goes here') + self.assertIsInstance(response, list) + self.assertTrue(all(isinstance(item, float) for item in response)) + + def test_completion_model(self): + deployment = self.proxy_client.select_deployment(model_name='gpt-4-instruct') + with openai_completion_mocker(deployment.prediction_url): + llm = OpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4-instruct') + self.assertIsNotNone(llm.model_name) + response = llm.invoke('Your text string goes here') + self.assertIsInstance(response, str) + + def test_chat_model_cohere(self, model_name='cohere--command-a-reasoning'): + deployment = self.proxy_client.select_deployment(model_name=model_name) + + with cohere_chat_completion_mocker(deployment.prediction_url): + self._test_chat_model(model_name) + + def _test_chat_model(self, model_name: str): + chat_model = ChatOpenAI(proxy_client=self.proxy_client, proxy_model_name=model_name) + self.assertIsNotNone(chat_model.model_name) + template = 'You are a helpful assistant that translates english to pirate.' + + system_message_prompt = SystemMessagePromptTemplate.from_template(template) + + example_human = HumanMessagePromptTemplate.from_template('Hi') + example_ai = AIMessagePromptTemplate.from_template('Ahoy!') + human_template = '{text}' + + human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) + chat_prompt = ChatPromptTemplate.from_messages( + [system_message_prompt, example_human, example_ai, human_message_prompt]) + + chain = LLMChain(llm=chat_model, prompt=chat_prompt) + response = chain.invoke('I love programming') + self.assertIsInstance(response['text'], str) + + def test_chat_model(self, model_name='gpt-4o-mini'): + deployment = self.proxy_client.select_deployment(model_name=model_name) + + with openai_chat_completion_mocker(deployment.prediction_url): + self._test_chat_model(model_name) + + def test_client_params(self): + with self.assertRaises(ValueError): + OpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4-instruct', n=0) + with self.assertRaises(ValueError): + OpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4-instruct', n=2, streaming=True) + with self.assertRaises(ValueError): + ChatOpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4o-mini', n=0) + with self.assertRaises(ValueError): + ChatOpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4o-mini', n=2, streaming=True) + + def test_max_completion_token(self): + unexpected_keyword_exception_message = "got an unexpected keyword argument" + with self.assertRaises(TypeError) as cm: + chat_open_api = ChatOpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4o-mini', n=1, + unexpected_token_kwarg=1) + chat_open_api.invoke('test invocation') + self.assertIsInstance(cm.exception, TypeError) + self.assertIn(unexpected_keyword_exception_message, str(cm.exception)) + + with self.assertRaises(BaseException) as cm: + chat_open_api = ChatOpenAI(proxy_client=self.proxy_client, proxy_model_name='gpt-4o-mini', n=1, + max_completion_tokens=1) + chat_open_api.invoke('test invocation') + self.assertNotIn(unexpected_keyword_exception_message, str(cm.exception)) + + def test_chat_openai_preserves_root_client(self): + """Test that ChatOpenAI preserves the root client when using native OpenAI client""" + from gen_ai_hub.proxy.native.openai import OpenAI as NativeOpenAI + + native_client = NativeOpenAI(proxy_client=self.proxy_client) + + chat_model = ChatOpenAI( + client=native_client, + proxy_client=self.proxy_client, + proxy_model_name='gpt-4o-mini' + ) + + # Verify root_client is preserved + self.assertTrue(hasattr(chat_model, 'root_client')) + self.assertEqual(chat_model.root_client, native_client) + + # Verify client points to chat.completions + self.assertEqual(chat_model.client, native_client.chat.completions) + + def test_chat_openai_preserves_root_async_client(self): + """Test that ChatOpenAI preserves the root async client when using native async OpenAI client""" + from gen_ai_hub.proxy.native.openai import AsyncOpenAI as NativeAsyncOpenAI + + native_async_client = NativeAsyncOpenAI(proxy_client=self.proxy_client) + + chat_model = ChatOpenAI( + async_client=native_async_client, + proxy_client=self.proxy_client, + proxy_model_name='gpt-4o-mini' + ) + + # Verify root_async_client is preserved + self.assertTrue(hasattr(chat_model, 'root_async_client')) + self.assertEqual(chat_model.root_async_client, native_async_client) + + # Verify async_client points to chat.completions + self.assertEqual(chat_model.async_client, native_async_client.chat.completions) + + def test_openai_completion_preserves_root_client(self): + """Test that OpenAI completion model preserves the root client""" + from gen_ai_hub.proxy.native.openai import OpenAI as NativeOpenAI + + native_client = NativeOpenAI(proxy_client=self.proxy_client) + + completion_model = OpenAI( + client=native_client, + proxy_client=self.proxy_client, + proxy_model_name='gpt-4o-mini' + ) + + # Verify root_client is preserved (it may be in model_kwargs due to pydantic validation) + self.assertTrue(hasattr(completion_model, 'root_client') or 'root_client' in completion_model.model_kwargs) + if hasattr(completion_model, 'root_client'): + self.assertEqual(completion_model.root_client, native_client) + else: + self.assertEqual(completion_model.model_kwargs['root_client'], native_client) + + # Verify client points to completions + self.assertEqual(completion_model.client, native_client.completions) + + def test_openai_completion_preserves_root_async_client(self): + """Test that OpenAI completion model preserves the root async client""" + from gen_ai_hub.proxy.native.openai import AsyncOpenAI as NativeAsyncOpenAI + + native_async_client = NativeAsyncOpenAI(proxy_client=self.proxy_client) + + completion_model = OpenAI( + async_client=native_async_client, + proxy_client=self.proxy_client, + proxy_model_name='gpt-4o-mini' + ) + + # Verify root_async_client is preserved (it may be in model_kwargs due to pydantic validation) + self.assertTrue( + hasattr(completion_model, 'root_async_client') or 'root_async_client' in completion_model.model_kwargs) + if hasattr(completion_model, 'root_async_client'): + self.assertEqual(completion_model.root_async_client, native_async_client) + else: + self.assertEqual(completion_model.model_kwargs['root_async_client'], native_async_client) + + # Verify async_client points to completions + self.assertEqual(completion_model.async_client, native_async_client.completions) diff --git a/packages/gen/tests/proxy/native/test_amazon.py b/packages/gen/tests/proxy/native/test_amazon.py new file mode 100644 index 0000000..0b994d1 --- /dev/null +++ b/packages/gen/tests/proxy/native/test_amazon.py @@ -0,0 +1,437 @@ +import json +import time +import unittest +from typing import Iterator +from unittest.mock import AsyncMock, MagicMock, patch + +from botocore.config import Config +from aiobotocore.config import AioConfig + +from gen_ai_hub.proxy.native.amazon.clients import Session, ClientWrapper, AsyncSession, AsyncClientWrapper +from tests.mock import (AMAZON_BEDROCK_RESPONSE, + AMAZON_BEDROCK_BROKEN_STREAM_RESPONSE, + get_mocked_ai_core_client, + ) + + +def get_bedrock_messages_inference_config(): + return {"maxTokens": 512, "temperature": 0.5, "topP": 0.9} + + +def get_bedrock_messages(): + return [ + { + "role": "user", + "content": [ + { + "text": "Describe the purpose of a 'hello world' program in one line." + } + ], + } + ] + + +def get_bedrock_prompt(): + return json.dumps( + { + "inputText": "Explain black holes to 8th graders.", + "textGenerationConfig": { + "maxTokenCount": 3072, + "stopSequences": [], + "temperature": 0.7, + "topP": 0.9, + }, + } + ) + +def get_delayed_bedrock_response(delay) -> Iterator[bytes]: + response = AMAZON_BEDROCK_RESPONSE + for line in response: + yield line + time.sleep(delay) # Simulate delay + + +class TestAmazonModels(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + cls.proxy_client._get_scenario_deployments = MagicMock() + cls.http_mock = MagicMock() + cls.http_mock.status_code = 200 + cls.parsed_response_mock = MagicMock() + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_completion(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.invoke_model(body=get_bedrock_prompt()) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "invoke") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_completion_streaming(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.invoke_model_with_response_stream(body=get_bedrock_prompt()) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "invoke-with-response-stream") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke-with-response-stream", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_completion_streaming_with_deprecated_timeout(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + with self.assertWarns(DeprecationWarning): + response = bedrock.invoke_model_with_response_stream(body=get_bedrock_prompt(), timeout=120) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "invoke-with-response-stream") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke-with-response-stream", + ) + + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_completion_stream_chunks(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.invoke_model_with_response_stream( + body=get_bedrock_prompt() + ) + + stream = response["body"] + self.assertTrue(all(isinstance(json.loads(event["chunk"]["bytes"])["type"], str) for event in stream)) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_broken_stream_chunks(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, AMAZON_BEDROCK_BROKEN_STREAM_RESPONSE) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.invoke_model_with_response_stream( + body=get_bedrock_prompt() + ) + + with self.assertRaises(ValueError): + for event in response['body']: + json.loads(event["chunk"]["bytes"]) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_stream_is_not_buffered(self, mock_make_request): + delay = 1 + self.http_mock.iter_lines.return_value = ( + get_delayed_bedrock_response(1) + ) + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.invoke_model_with_response_stream( + body=get_bedrock_prompt() + ) + + stream = response["body"] + times = [] + for event in stream: + times.append(time.time()) + self.assertIsInstance(json.loads(event["chunk"]["bytes"])["type"], str) + for i in range(1, len(times)): + self.assertGreaterEqual(times[i] - times[i - 1], delay - 0.25) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_chat_completion(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.converse( + messages=get_bedrock_messages(), + inferenceConfig=get_bedrock_messages_inference_config(), + ) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "converse") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/converse", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.BaseClient._make_request") + def test_chat_converse_streaming(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = Session().client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = bedrock.converse_stream( + messages=get_bedrock_messages(), + inferenceConfig=get_bedrock_messages_inference_config(), + ) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "converse-stream") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/converse-stream", + ) + + def test_config_parameters(self): + custom_config = Config({'temperature': 0}) + self.assertIsInstance( + Session().client(model_name="amazon--nova-premier", + proxy_client=self.proxy_client, + region_name='us', config=custom_config), + ClientWrapper + ) + + def test_service_name(self): + with self.assertRaises(NotImplementedError): + Session().client(model_name="amazon--nova-premier", + proxy_client=self.proxy_client, + service_name="Claude") + + +class TestAsyncClientWrapper(unittest.IsolatedAsyncioTestCase): + + @classmethod + def setUpClass(cls) -> None: + cls.proxy_client = get_mocked_ai_core_client() + cls.proxy_client._get_scenario_deployments = MagicMock() + cls.http_mock = MagicMock() + cls.http_mock.status_code = 200 + cls.parsed_response_mock = MagicMock() + + async def test_config_parameters_for_async(self): + custom_config = AioConfig( + read_timeout=120, + connect_timeout=120, + ) + res_client = await AsyncSession().async_client(model_name="amazon--nova-premier", + proxy_client=self.proxy_client, + region_name='us', config=custom_config) + self.assertIsInstance( + res_client, + AsyncClientWrapper + ) + self.assertEqual(res_client._client_config.read_timeout, 120) + self.assertEqual(res_client._client_config.connect_timeout, 120) + + async def test_async_client(self): + with self.assertRaises(NotImplementedError): + await AsyncSession().async_client(model_name="amazon--nova-premier", + proxy_client=self.proxy_client, + service_name="Claude") + + @patch("gen_ai_hub.proxy.native.amazon.clients.AioBaseClient._make_request") + async def test_async_invoke_model(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = await bedrock.invoke_model(body=get_bedrock_prompt()) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "invoke") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.AioBaseClient._make_request") + async def test_async_converse(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = await bedrock.converse( + messages=get_bedrock_messages(), + inferenceConfig=get_bedrock_messages_inference_config(), + ) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "converse") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/converse", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.AioBaseClient._make_request") + async def test_async_converse_streaming(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = await bedrock.converse_stream( + messages=get_bedrock_messages(), + inferenceConfig=get_bedrock_messages_inference_config(), + ) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "converse-stream") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/converse-stream", + ) + + + @patch("gen_ai_hub.proxy.native.amazon.clients.AioBaseClient._make_request") + async def test_async_invoke_streaming(self, mock_make_request): + mock_make_request.return_value = (self.http_mock, self.parsed_response_mock) + + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + response = await bedrock.invoke_model_with_response_stream(body=get_bedrock_prompt()) + self.assertIsInstance(response, MagicMock) + mock_make_request.assert_called_once() + self.assertEqual(mock_make_request.call_args.args[1]["url_path"], "invoke-with-response-stream") + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke-with-response-stream", + ) + + @patch("gen_ai_hub.proxy.native.amazon.clients.AioBaseClient._make_request") + async def test_async_invoke_streaming_reads_response_on_error(self, mock_make_request): + mock_make_request.side_effect = Exception("Error") + + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", proxy_client=self.proxy_client + ) + with self.assertRaises(Exception) as context: + await bedrock.invoke_model_with_response_stream(body=get_bedrock_prompt()) + + self.assertEqual(str(context.exception), "Error") + + mock_make_request.assert_called_once() + deployment = self.proxy_client.select_deployment( + model_name="amazon--nova-premier" + ) + + self.assertEqual( + mock_make_request.call_args.args[1]["url"], + deployment.url + "/invoke-with-response-stream", + ) + + async def test_cleanup_handles_exceptions_gracefully(self): + """Test that _cleanup() handles exceptions without raising.""" + # Create a real client first + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", + proxy_client=self.proxy_client + ) + + # Replace the context manager with a mock that raises on __aexit__ + mock_context_manager = MagicMock() + mock_aexit = AsyncMock(side_effect=Exception("Cleanup error")) + mock_context_manager.__aexit__ = mock_aexit + bedrock._context_manager = mock_context_manager + + # Call cleanup and verify it doesn't raise + try: + await bedrock._close() + cleanup_succeeded = True + except Exception: + cleanup_succeeded = False + + self.assertTrue(cleanup_succeeded, "_cleanup() should handle exceptions gracefully") + # Verify __aexit__ was called despite the exception + mock_aexit.assert_called_once_with(None, None, None) + # Verify context manager was set to None + self.assertIsNone(bedrock._context_manager) + + async def test_close_calls_cleanup(self): + """Test that close() properly calls _cleanup().""" + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", + proxy_client=self.proxy_client + ) + + # Replace context manager with a mock to track calls + mock_context_manager = MagicMock() + mock_aexit = AsyncMock() + mock_context_manager.__aexit__ = mock_aexit + bedrock._context_manager = mock_context_manager + + # Call close explicitly + await bedrock.close() + + # Verify __aexit__ was called + mock_aexit.assert_called_once_with(None, None, None) + # Verify context manager was set to None + self.assertIsNone(bedrock._context_manager) + + async def test_context_manager_cleanup(self): + """Test that using async with properly cleans up.""" + # Create client and replace context manager with mock + bedrock = await AsyncSession().async_client( + model_name="amazon--nova-premier", + proxy_client=self.proxy_client + ) + + mock_context_manager = MagicMock() + mock_aexit = AsyncMock() + mock_context_manager.__aexit__ = mock_aexit + original_context_manager = bedrock._context_manager + bedrock._context_manager = mock_context_manager + + # Use async with context manager + async with bedrock: + # Client is usable inside context + self.assertIsInstance(bedrock, AsyncClientWrapper) + + # Verify __aexit__ was called after context exit + mock_aexit.assert_called_once_with(None, None, None) + # Verify context manager was set to None + self.assertIsNone(bedrock._context_manager) + + # Clean up the original context manager + await original_context_manager.__aexit__(None, None, None) + +def test_flat_import_amazon(): + from gen_ai_hub.proxy.native.amazon import ClientWrapper as client_wrapper_flat_import + from gen_ai_hub.proxy.native.amazon.clients import ClientWrapper as client_wrapper_nested_import + assert client_wrapper_flat_import == client_wrapper_nested_import \ No newline at end of file diff --git a/packages/gen/tests/proxy/native/test_google_genai_clients.py b/packages/gen/tests/proxy/native/test_google_genai_clients.py new file mode 100644 index 0000000..fc9b84f --- /dev/null +++ b/packages/gen/tests/proxy/native/test_google_genai_clients.py @@ -0,0 +1,103 @@ +import unittest +from unittest.mock import MagicMock, patch +import httpx +from gen_ai_hub.proxy.native.google_genai.clients import ( + _rewrite_request, + AICoreDynamicTransport, + AsyncAICoreDynamicTransport, +) + +class TestRewriteRequest(unittest.TestCase): + def setUp(self): + self.transport_instance = MagicMock() + self.transport_instance.proxy_client.request_header = { + "Authorization": "Bearer test-token", + "Custom-Header": "CustomValue" + } + self.transport_instance.proxy_client.select_deployment.return_value = MagicMock( + url="https://base_url.com/deployment" + ) + self.transport_instance.get_selector_kwargs.return_value = {} + + def test_rewrite_request_with_valid_model(self): + request = httpx.Request( + method="POST", + url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + ) + modified_request = _rewrite_request(self.transport_instance, request) + + self.assertEqual(modified_request.url.host, "base_url.com") + self.assertIn("/models/test-model:generateContent", modified_request.url.path) + self.assertEqual(modified_request.headers["Authorization"], "Bearer test-token") + self.assertEqual(modified_request.headers["Custom-Header"], "CustomValue") + + def test_rewrite_request_with_discovery_call(self): + request = httpx.Request( + method="GET", + url=httpx.URL("https://api.base_url.com/models/"), + ) + modified_request = _rewrite_request(self.transport_instance, request) + + self.assertEqual(modified_request.url.host, "api.base_url.com") + self.assertEqual(modified_request.url.path, "/models/") + +class TestAICoreDynamicTransport(unittest.TestCase): + def setUp(self): + self.proxy_client = MagicMock() + self.transport = AICoreDynamicTransport(proxy_client=self.proxy_client) + self.transport.proxy_client.select_deployment.return_value = MagicMock( + url="https://base_url.com/deployment" + ) + + @patch("httpx.HTTPTransport.handle_request") + def test_handle_request(self, mock_handle_request): + request = httpx.Request( + method="POST", + url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + ) + mock_response = httpx.Response(200, text="Success") + mock_handle_request.return_value = mock_response + + response = self.transport.handle_request(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "Success") + mock_handle_request.assert_called_once() + + def test_close(self): + with patch.object(self.transport._inner_transport, "close") as mock_close: + self.transport.close() + mock_close.assert_called_once() + +class TestAsyncAICoreDynamicTransport(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.proxy_client = MagicMock() + self.transport = AsyncAICoreDynamicTransport(proxy_client=self.proxy_client) + self.transport.proxy_client.select_deployment.return_value = MagicMock( + url="https://base_url.com/deployment" + ) + + @patch("httpx.AsyncHTTPTransport.handle_async_request") + async def test_handle_async_request(self, mock_handle_async_request): + request = httpx.Request( + method="POST", + url=httpx.URL("https://api.base_url.com/models/test-model:generateContent"), + ) + mock_response = httpx.Response(200, text="Success") + mock_handle_async_request.return_value = mock_response + + response = await self.transport.handle_async_request(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "Success") + mock_handle_async_request.assert_called_once() + + async def test_aclose(self): + with patch.object(self.transport._inner_transport, "aclose") as mock_aclose: + await self.transport.aclose() + mock_aclose.assert_called_once() + +def test_flat_import_google_genai_clients(): + from gen_ai_hub.proxy.native.google_genai.clients import Client as client + from gen_ai_hub.proxy.native.google_genai import Client as client_flat + assert client == client_flat diff --git a/packages/gen/tests/proxy/native/test_openai.py b/packages/gen/tests/proxy/native/test_openai.py new file mode 100644 index 0000000..ec8aac4 --- /dev/null +++ b/packages/gen/tests/proxy/native/test_openai.py @@ -0,0 +1,294 @@ +import unittest +from unittest.mock import MagicMock +from pydantic import BaseModel + +import openai +from openai.types import Completion, CreateEmbeddingResponse +from openai.types.chat import ChatCompletion, ChatCompletionUserMessageParam +from openai.types.responses import Response + +from gen_ai_hub.proxy.native.openai import OpenAI, AsyncOpenAI +from gen_ai_hub.proxy.native.openai.clients import ChatCompletions, AsyncChatCompletions, Responses, AsyncResponses +from integration_tests.constants import OPENAI_GPT_4O_MINI_TEST_MODEL, NVIDIA_EMBEDDING_TEST_MODEL + +from tests.mock import ( + get_mocked_ai_core_client, + openai_chat_completion_mocker, + openai_stream_completion_mocker, + openai_embeddings_mocker, openai_structured_outputs_mocker, + openai_responses_mocker, openai_responses_structured_outputs_mocker +) +from tests.proxy.langchain_.test_init_models import TestInitModels + + +class TestOpenAIModels(unittest.TestCase): + + def setUp(self) -> None: + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + self.messages = [{ + 'role': 'system', + 'content': 'You are a helpful assistant.' + }, { + 'role': 'user', + 'content': 'Say hi, in one word!' + }] + self.kwargs = {'model_name': OPENAI_GPT_4O_MINI_TEST_MODEL, 'messages': self.messages} + + def test_chat_stream_completion(self): + deployment = self.proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + with openai_stream_completion_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + chunks = openai_client.chat.completions.create(**self.kwargs) + self.assertTrue(all('"object": "chat.completion.chunk"' in chunk for chunk in chunks)) + + def test_chat_completion(self): + deployment = self.proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + with openai_chat_completion_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + response = openai_client.chat.completions.create(**self.kwargs) + self.assertIsInstance(response, ChatCompletion) + response = openai_client.with_raw_response.chat.completions.create(**self.kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_completion(self): + deployment = self.proxy_client.select_deployment(model_name='gpt-4-instruct') + with openai_chat_completion_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model_name': 'gpt-4-instruct', 'prompt': 'Say this is a test', 'max_tokens': 7, 'temperature': 0} + response = openai_client.completions.create(**kwargs) + self.assertIsInstance(response, Completion) + response = openai_client.with_raw_response.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_completion_with_custom_model(self): + model_name = 'test-custom-openai-model' + custom_deployment = TestInitModels.create_deployment(model_name) + self.proxy_client._deployments.append(custom_deployment) + with openai_chat_completion_mocker(f'{custom_deployment.url}/completions'): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model_name': model_name, 'prompt': 'Say this is a test', 'max_tokens': 7, 'temperature': 0} + response = openai_client.completions.create(**kwargs) + self.assertIsInstance(response, Completion) + response = openai_client.with_raw_response.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + def test_update_deployments_called_for_custom_model(self): + model_name = 'test-custom-openai-model-2' + custom_deployment = TestInitModels.create_deployment(model_name) + deployments = TestInitModels.create_gen_ai_hub_deployments() + deployments.append(custom_deployment) + deployments_dict = {} + for i in range(len(deployments)): + deployments_dict[i] = deployments[i] + self.proxy_client._get_scenario_deployments = MagicMock() + self.proxy_client._get_scenario_deployments.return_value = deployments_dict + with openai_chat_completion_mocker(f'{custom_deployment.url}/completions'): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model_name': model_name, 'prompt': 'Say this is a test', 'max_tokens': 7, 'temperature': 0} + response = openai_client.completions.create(**kwargs) + self.assertIsInstance(response, Completion) + response = openai_client.with_raw_response.completions.create(**kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + self.proxy_client._get_scenario_deployments.assert_called_once_with( + self.proxy_client.foundational_model_scenarios[0]) + + def test_embeddings(self): + deployment = self.proxy_client.select_deployment(model_name='text-embedding-ada-002') + with openai_embeddings_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model': 'text-embedding-ada-002', 'input': 'Your text string goes here'} + response = openai_client.embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + + def test_nvidia_embeddings(self, model=NVIDIA_EMBEDDING_TEST_MODEL): + input_type = ['query', 'passage'] + for it in input_type: + deployment = self.proxy_client.select_deployment(model_name=model) + with openai_embeddings_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model': model, 'input': 'Your text string goes here', + 'extra_body': {'input_type': it}} + response = openai_client.embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + + def test_embeddings_with_custom_model(self): + model_name = 'test-custom-openai-embedding-model' + custom_deployment = TestInitModels.create_deployment(model_name) + self.proxy_client._deployments.append(custom_deployment) + with openai_embeddings_mocker(custom_deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model': model_name, 'input': 'Your text string goes here'} + response = openai_client.embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + + # region Tests for structured_outputs = response in json format + """ Tests for structured outputs in OpenAI client + https://platform.openai.com/docs/guides/structured-outputs + """ + + class PersonName(BaseModel): + first_name: str + last_name: str + + def test_openai_client_beta_attribute_set_to_self(self): + """Test that OpenAI client beta attribute is set to self""" + openai_client = OpenAI(proxy_client=self.proxy_client) + + # Verify beta attribute is set to self + self.assertEqual(openai_client.beta, openai_client) + self.assertIsNotNone(openai_client.beta) + + """Test that AsyncOpenAI client beta attribute is set to self""" + async_openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + + # Verify beta attribute is set to self + self.assertEqual(async_openai_client.beta, async_openai_client) + self.assertIsNotNone(async_openai_client.beta) + + def test_beta_client_enables_structured_output_access(self): + """Test that beta client enables access to structured output features""" + openai_client = OpenAI(proxy_client=self.proxy_client) + + # Verify beta client has the same chat completions interface as main client + self.assertEqual(openai_client.beta.chat.completions, openai_client.chat.completions) + + # Verify beta parse method is accessible + self.assertTrue(hasattr(openai_client.beta.chat.completions, 'parse')) + self.assertTrue(callable(openai_client.beta.chat.completions.parse)) + + """Test that async beta client enables access to structured output features""" + async_openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + + # Verify beta client has the same chat completions interface as main client + self.assertEqual(async_openai_client.beta.chat.completions, async_openai_client.chat.completions) + + # Verify beta parse method is accessible + self.assertTrue(hasattr(async_openai_client.beta.chat.completions, 'parse')) + self.assertTrue(callable(async_openai_client.beta.chat.completions.parse)) + + def test_chat_completions_parse_method_exists(self): + """Test that the parse method exists on ChatCompletions""" + openai_client = OpenAI(proxy_client=self.proxy_client) + chat_completions = openai_client.chat.completions + + # Verify parse method exists and is callable + self.assertTrue(hasattr(chat_completions, 'parse')) + self.assertTrue(callable(chat_completions.parse)) + self.assertIsInstance(chat_completions, ChatCompletions) + + """Test that the async parse method exists on AsyncChatCompletions""" + async_openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + async_chat_completions = async_openai_client.chat.completions + + # Verify async parse method exists and is callable + self.assertTrue(hasattr(async_chat_completions, 'parse')) + self.assertTrue(callable(async_chat_completions.parse)) + self.assertIsInstance(async_chat_completions, AsyncChatCompletions) + + def test_chat_completions_parse_with_response_format(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + """Test that parse method handles response_format parameter correctly""" + + deployment = self.proxy_client.select_deployment(model_name=model) + with openai_structured_outputs_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + response = openai_client.chat.completions.parse( + model=model, + messages=ChatCompletionUserMessageParam(role="user", content="Tell me about John Doe."), + response_format=self.PersonName + ) + self.assertIsInstance(response, ChatCompletion) + + async def test_async_chat_completions_parse_with_response_format(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + """Test that async parse method works with a Pydantic model for structured output""" + + deployment = self.proxy_client.select_deployment(model_name=model) + with openai_structured_outputs_mocker(deployment.prediction_url): + openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + response = await openai_client.chat.completions.parse( + model=model, + messages=ChatCompletionUserMessageParam(role="user", content="Tell me about John Doe."), + response_format=self.PersonName + ) + self.assertIsInstance(response, ChatCompletion) + + # endregion + + def test_responses(self): + deployment = self.proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + with openai_responses_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + kwargs = {'model_name': OPENAI_GPT_4O_MINI_TEST_MODEL, 'input': 'Say this is a test'} + response = openai_client.responses.create(**kwargs) + self.assertIsInstance(response, Response) + + def test_responses_parse_method_exists(self): + """Test that the parse method exists on Responses""" + openai_client = OpenAI(proxy_client=self.proxy_client) + responses = openai_client.responses + + # Verify parse method exists and is callable + self.assertTrue(hasattr(responses, 'parse')) + self.assertTrue(callable(responses.parse)) + self.assertIsInstance(responses, Responses) + + """Test that the async parse method exists on AsyncResponses""" + async_openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + async_responses = async_openai_client.responses + + # Verify async parse method exists and is callable + self.assertTrue(hasattr(async_responses, 'parse')) + self.assertTrue(callable(async_responses.parse)) + self.assertIsInstance(async_responses, AsyncResponses) + + def test_responses_parse_with_text_format(self, model=OPENAI_GPT_4O_MINI_TEST_MODEL): + """Test that parse method handles text_format parameter correctly""" + + deployment = self.proxy_client.select_deployment(model_name=model) + with openai_responses_structured_outputs_mocker(deployment.prediction_url): + openai_client = OpenAI(proxy_client=self.proxy_client) + response = openai_client.responses.parse( + model=model, + input="Tell me about John Doe aged 30.", + text_format=self.PersonName + ) + self.assertIsInstance(response, Response) + + +class TestAsyncOpenAIModels(unittest.IsolatedAsyncioTestCase): + + def setUp(self) -> None: + self.proxy_client = get_mocked_ai_core_client(client_id='testasyncopenaiclient') + self.messages = [{ + 'role': 'system', + 'content': 'You are a helpful assistant.' + }, { + 'role': 'user', + 'content': 'Say hi, in one word!' + }] + self.kwargs = {'model_name': OPENAI_GPT_4O_MINI_TEST_MODEL, 'messages': self.messages} + + async def test_async_chat_completions(self): + deployment = self.proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + with openai_chat_completion_mocker(deployment.prediction_url): + openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + response = await openai_client.chat.completions.create(**self.kwargs) + self.assertIsInstance(response, ChatCompletion) + response = await openai_client.with_raw_response.chat.completions.create(**self.kwargs) + self.assertIsInstance(response, openai._legacy_response.LegacyAPIResponse) + + async def test_async_nvidia_embeddings(self, model=NVIDIA_EMBEDDING_TEST_MODEL): + deployment = self.proxy_client.select_deployment(model_name=model) + with openai_embeddings_mocker(deployment.prediction_url): + openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + kwargs = {'model': model, 'input': 'Your text string goes here', + 'extra_body': {'input_type': 'query'}} + response = await openai_client.embeddings.create(**kwargs) + self.assertIsInstance(response, CreateEmbeddingResponse) + + async def test_async_responses(self): + deployment = self.proxy_client.select_deployment(model_name=OPENAI_GPT_4O_MINI_TEST_MODEL) + with openai_responses_mocker(deployment.prediction_url): + openai_client = AsyncOpenAI(proxy_client=self.proxy_client) + kwargs = {'model_name': OPENAI_GPT_4O_MINI_TEST_MODEL, 'input': 'Say this is a test'} + response = await openai_client.responses.create(**kwargs) + self.assertIsInstance(response, Response) diff --git a/packages/gen/tests/proxy/native/test_sap_rpt.py b/packages/gen/tests/proxy/native/test_sap_rpt.py new file mode 100644 index 0000000..9e7bd07 --- /dev/null +++ b/packages/gen/tests/proxy/native/test_sap_rpt.py @@ -0,0 +1,209 @@ +import unittest +from unittest.mock import patch + +from gen_ai_hub.proxy.native.sap.models import RPTRequest, PredictionConfig, TargetColumn, RPTResponse, RPTException +from gen_ai_hub.proxy.native.sap.client import RPTClient +from tests.mock import get_mocked_ai_core_client, sap_rpt_moke_response_code_0, sap_rpt_moke_response_code_2 + +mock_url = "https://mock-rpt-deployment" + +request_by_row_dict = { + "prediction_config": { + "target_columns": [ + { + "name": "COSTCENTER", + "prediction_placeholder": "[PREDICT]", + "task_type": "classification" + } + ] + }, + "index_column": "ID", + "rows": [ + { + "PRODUCT": "Couch", + "PRICE": 999.99, + "ORDERDATE": "28-11-2025", + "ID": "35", + "COSTCENTER": "[PREDICT]" + }, + { + "PRODUCT": "Office Chair", + "PRICE": 150.8, + "ORDERDATE": "02-11-2025", + "ID": "44", + "COSTCENTER": "Office Furniture" + }, + { + "PRODUCT": "Server Rack", + "PRICE": 2200.00, + "ORDERDATE": "01-11-2025", + "ID": "104", + "COSTCENTER": "Data Infrastructure" + } + ], + "data_schema": { + "PRODUCT": { + "dtype": "string" + }, + "PRICE": { + "dtype": "numeric" + }, + "ORDERDATE": { + "dtype": "date" + }, + "ID": { + "dtype": "string" + }, + "COSTCENTER": { + "dtype": "string" + } + } + } + +request_by_columns_dict = { + "prediction_config": { + "target_columns": [ + { + "name": "COSTCENTER", + "prediction_placeholder": "[PREDICT]", + "task_type": "classification" + } + ] + }, + "index_column": "ID", + "columns": { + "PRODUCT": ["Couch", "Office Chair", "Server Rack"], + "PRICE": [999.99, 150.8, 2200.00], + "ORDERDATE": ["28-11-2025", "02-11-2025", "01-11-2025"], + "ID": ["35", "44", "104"], + "COSTCENTER": ["[PREDICT]", "Office Furniture", "Data Infrastructure"] + }, + "data_schema": { + "PRODUCT": { + "dtype": "string" + }, + "PRICE": { + "dtype": "numeric" + }, + "ORDERDATE": { + "dtype": "date" + }, + "ID": { + "dtype": "string" + }, + "COSTCENTER": { + "dtype": "string" + } + } +} + +class RPTRequestModels(unittest.TestCase): + + def test_prediction_config(self): + expected_dict = { + "target_columns": [ + { + "name": "COSTCENTER", + "prediction_placeholder": "[PREDICT]", + "task_type": "classification" + }] + } + prediction_config = PredictionConfig(target_columns=[ + TargetColumn(name="COSTCENTER", prediction_placeholder="[PREDICT]", task_type="classification") + ]) + + assert prediction_config.model_dump() == expected_dict + + def test_rpt_request_by_rows_from_dict(self): + + request = RPTRequest.model_validate(request_by_row_dict) + assert request.prediction_config.target_columns[0].name == "COSTCENTER" + assert request.rows[0]["COSTCENTER"] == "[PREDICT]" + assert "columns" not in request.model_dump() + + def test_rpt_request_by_columns_from_dict(self): + request = RPTRequest.model_validate(request_by_columns_dict) + assert request.prediction_config.target_columns[0].name == "COSTCENTER" + assert request.columns["COSTCENTER"][0] == "[PREDICT]" + assert "rows" not in request.model_dump() + + def test_rpt_request_columns_and_rows_provided(self): + with self.assertRaises(ValueError) as err: + RPTRequest( + prediction_config=request_by_row_dict["prediction_config"], + columns=request_by_columns_dict["columns"], + rows=request_by_row_dict["rows"] + ) + assert "Exactly one of 'rows' or 'columns' must be provided." in str(err.exception) + +class RPTClientTests(unittest.TestCase): + + def setUp(self): + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + self.client = RPTClient(proxy_client=self.proxy_client) + + def test_request_with_response_code_0(self): + with patch.object(RPTClient, "_get_url", return_value=mock_url) as url_mock: + with sap_rpt_moke_response_code_0(url_mock.return_value): + response = self.client.predict(body=request_by_row_dict, model_name="sap-rpt-1-small") + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.predictions[0]["COSTCENTER"][0].prediction, "Office Furniture") + self.assertEqual(response.metadata.num_columns,5) + self.assertEqual(response.metadata.num_predictions,1) + + def test_request_with_response_code_0_request_by_api_url(self): + with sap_rpt_moke_response_code_0(mock_url): + response = self.client.predict(body=request_by_row_dict, deployment_url=mock_url) + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.id, "c334f854-0d70-4c79-bd73-9ac581fd8cda") + + + def test_request_with_response_code_2(self): + with patch.object(RPTClient, "_get_url", return_value=mock_url) as url_mock: + with sap_rpt_moke_response_code_2(url_mock.return_value): + with self.assertRaises(RPTException) as err: + self.client.predict(body=request_by_row_dict, model_name="sap-rpt-1-small") + self.assertEqual(err.exception.status.code, 2) + self.assertIsNotNone(err.exception.detail) + + def test_request_with_invalid_body(self): + with self.assertRaises(ValueError): + self.client.predict(body={}, model_name="sap-rpt-1-small") + + def test_request_without_model_name_api_url_and_kwargs(self): + with self.assertRaises(ValueError): + self.client.predict(body=request_by_row_dict) + + def test_timeout_determination(self): + self.assertEqual(self.client._determine_timeout(10), 10) + +class RPTClientAsyncTests(unittest.IsolatedAsyncioTestCase): + + def setUp(self): + self.proxy_client = get_mocked_ai_core_client(client_id='testopenaiclient') + self.client = RPTClient(proxy_client=self.proxy_client) + + async def test_async_request_with_response_code_0(self): + with patch.object(RPTClient, "_get_url", return_value=mock_url) as url_mock: + with sap_rpt_moke_response_code_0(url_mock.return_value): + response = await self.client.apredict(body=request_by_row_dict, model_name="sap-rpt-1-small") + self.assertIsInstance(response, RPTResponse) + self.assertEqual(response.status.code, 0) + self.assertEqual(response.predictions[0]["COSTCENTER"][0].prediction, "Office Furniture") + self.assertEqual(response.metadata.num_columns,5) + self.assertEqual(response.metadata.num_predictions,1) + + async def test_async_request_with_response_code_2(self): + with patch.object(RPTClient, "_get_url", return_value=mock_url) as url_mock: + with sap_rpt_moke_response_code_2(url_mock.return_value): + with self.assertRaises(RPTException) as err: + await self.client.apredict(body=request_by_row_dict, model_name="sap-rpt-1-small") + self.assertEqual(err.exception.status.code, 2) + self.assertIsNotNone(err.exception.detail) + +def test_flat_import_sap_rpt_client(): + from gen_ai_hub.proxy.native.sap.client import RPTClient as client + from gen_ai_hub.proxy.native.sap import RPTClient as client_flat + assert client == client_flat \ No newline at end of file diff --git a/packages/gen/tests/test_additional_headers.py b/packages/gen/tests/test_additional_headers.py new file mode 100644 index 0000000..f9306a5 --- /dev/null +++ b/packages/gen/tests/test_additional_headers.py @@ -0,0 +1,255 @@ +import unittest +from unittest.mock import MagicMock, patch + +from gen_ai_hub.proxy.gen_ai_hub_proxy.client import GenAIHubRestClient, temporary_headers_addition +from gen_ai_hub.prompt_registry.client import PromptTemplateClient +from gen_ai_hub.document_grounding.clients.pipeline_api_client import PipelineAPIClient +from gen_ai_hub.document_grounding.clients.retrieval_api_client import RetrievalAPIClient +from gen_ai_hub.document_grounding.clients.vector_api_client import VectorAPIClient +from gen_ai_hub.orchestration.service import OrchestrationService +from gen_ai_hub.orchestration.models.llm import LLM +from gen_ai_hub.orchestration.models.template import Template +from gen_ai_hub.orchestration.models.message import Message +from gen_ai_hub.orchestration.models.config import OrchestrationConfig +from gen_ai_hub.orchestration_v2.service import OrchestrationService as OrchestrationServiceV2 +from gen_ai_hub.orchestration_v2.models.template import Template as TemplateV2, PromptTemplatingModuleConfig +from gen_ai_hub.orchestration_v2.models.config import OrchestrationConfig as OrchestrationConfigV2, ModuleConfig +from gen_ai_hub.orchestration_v2.models.llm_model_details import LLMModelDetails +from gen_ai_hub.orchestration_v2.models.message import UserMessage +from tests.mock import get_mocked_ai_core_client + + +class TestGenAIHubRestClient(unittest.TestCase): + """Unit tests for GenAIHubRestClient header injection logic.""" + + def setUp(self): + self.mock_rest_client = MagicMock() + self.mock_proxy_client = MagicMock() + self.mock_proxy_client.ai_core_client.rest_client = self.mock_rest_client + self.mock_proxy_client.get_additional_headers.return_value = {'X-Custom': 'custom-value', 'X-Another': 'another-value'} + self.rest_client = GenAIHubRestClient(self.mock_proxy_client) + + def test_get_injects_headers(self): + """Test GET requests include additional headers.""" + self.rest_client.get('/test-path', params={'key': 'value'}) + + self.mock_rest_client.get.assert_called_once_with( + path='/test-path', + params={'key': 'value'}, + headers={'X-Custom': 'custom-value', 'X-Another': 'another-value'} + ) + + def test_post_injects_headers(self): + """Test POST requests include additional headers.""" + self.rest_client.post('/test-path', body={'data': 'test'}) + + self.mock_rest_client.post.assert_called_once_with( + path='/test-path', + body={'data': 'test'}, + headers={'X-Custom': 'custom-value', 'X-Another': 'another-value'} + ) + + def test_delete_injects_headers(self): + """Test DELETE requests include additional headers.""" + self.rest_client.delete('/test-path') + + self.mock_rest_client.delete.assert_called_once_with( + path='/test-path', + headers={'X-Custom': 'custom-value', 'X-Another': 'another-value'} + ) + + def test_patch_injects_headers(self): + """Test PATCH requests include additional headers.""" + self.rest_client.patch('/test-path', body={'update': 'data'}) + + self.mock_rest_client.patch.assert_called_once_with( + path='/test-path', + body={'update': 'data'}, + headers={'X-Custom': 'custom-value', 'X-Another': 'another-value'} + ) + + def test_explicit_headers_merged(self): + """Test that explicit headers are merged with additional headers.""" + self.rest_client.get('/test-path', headers={'X-Explicit': 'explicit-value'}) + + self.mock_rest_client.get.assert_called_once() + call_kwargs = self.mock_rest_client.get.call_args[1] + self.assertEqual(call_kwargs['headers']['X-Custom'], 'custom-value') + self.assertEqual(call_kwargs['headers']['X-Another'], 'another-value') + self.assertEqual(call_kwargs['headers']['X-Explicit'], 'explicit-value') + + def test_explicit_headers_override_additional_headers(self): + """Test that explicit headers override additional headers.""" + self.rest_client.get('/test-path', headers={'X-Custom': 'overridden'}) + + call_kwargs = self.mock_rest_client.get.call_args[1] + self.assertEqual(call_kwargs['headers']['X-Custom'], 'overridden') + + def test_kwargs_propagated(self): + """Test that all kwargs are propagated to underlying rest_client.""" + self.rest_client.get('/test-path', params={'p': 1}, return_bytes_content=True) + + call_kwargs = self.mock_rest_client.get.call_args[1] + self.assertEqual(call_kwargs['params'], {'p': 1}) + self.assertTrue(call_kwargs['return_bytes_content']) + + def test_no_headers_when_no_additional_headers(self): + """Test that no headers kwarg is added when there are no additional headers.""" + self.mock_proxy_client.get_additional_headers.return_value = {} + self.rest_client.get('/test-path', params={'key': 'value'}) + + self.mock_rest_client.get.assert_called_once_with( + path='/test-path', + params={'key': 'value'} + ) + + +class TestClientHeaderInjection(unittest.TestCase): + """Test that each client properly uses GenAIHubRestClient for header injection.""" + + def setUp(self): + self.proxy_client = get_mocked_ai_core_client(client_id='test') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_prompt_template_client_injects_headers(self, mock_get): + """Test PromptTemplateClient passes headers via GenAIHubRestClient.""" + mock_get.return_value = {'count': 0, 'resources': []} + client = PromptTemplateClient(proxy_client=self.proxy_client) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + client.get_prompt_templates(scenario='s', name='n', version='v') + + call_kwargs = mock_get.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_pipeline_api_client_injects_headers(self, mock_get): + """Test PipelineAPIClient passes headers via GenAIHubRestClient.""" + mock_get.return_value = {'count': 0, 'resources': []} + client = PipelineAPIClient(proxy_client=self.proxy_client) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + client.get_pipelines() + + call_kwargs = mock_get.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_retrieval_api_client_injects_headers(self, mock_get): + """Test RetrievalAPIClient passes headers via GenAIHubRestClient.""" + mock_get.return_value = {'count': 0, 'resources': []} + client = RetrievalAPIClient(proxy_client=self.proxy_client) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + client.get_data_repositories() + + call_kwargs = mock_get.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + @patch('ai_api_client_sdk.helpers.rest_client.RestClient.get') + def test_vector_api_client_injects_headers(self, mock_get): + """Test VectorAPIClient passes headers via GenAIHubRestClient.""" + mock_get.return_value = {'count': 0, 'resources': []} + client = VectorAPIClient(proxy_client=self.proxy_client) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + client.get_collections() + + call_kwargs = mock_get.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + @patch('httpx.Client.post') + def test_orchestration_service_injects_headers(self, mock_post): + """Test OrchestrationService passes headers via request_header.""" + mock_response = MagicMock() + mock_response.json.return_value = { + 'request_id': 'test-id', + 'module_results': {}, + 'orchestration_result': { + 'id': 'test', + 'object': 'chat.completion', + 'created': 1234567890, + 'model': 'gpt-4', + 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'Hello'}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 10, 'completion_tokens': 5, 'total_tokens': 15} + } + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + config = OrchestrationConfig( + llm=LLM(name='gpt-4'), + template=Template(messages=[Message(role='user', content='Hello')]) + ) + service = OrchestrationService( + api_url='https://test.example.com', + proxy_client=self.proxy_client, + config=config + ) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + service.run() + + call_kwargs = mock_post.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + @patch('httpx.Client.post') + def test_orchestration_service_v2_injects_headers(self, mock_post): + """Test OrchestrationService V2 passes headers via request_header.""" + mock_response = MagicMock() + mock_response.json.return_value = { + 'request_id': 'test-id', + 'intermediate_results': {}, + 'final_result': { + 'id': 'test', + 'object': 'chat.completion', + 'created': 1234567890, + 'model': 'gpt-4', + 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': 'Hello'}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 10, 'completion_tokens': 5, 'total_tokens': 15} + } + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + config = OrchestrationConfigV2( + modules=ModuleConfig( + prompt_templating=PromptTemplatingModuleConfig( + prompt=TemplateV2(template=[UserMessage(content='Hello')]), + model=LLMModelDetails(name='gpt-4') + ) + ) + ) + service = OrchestrationServiceV2( + api_url='https://test.example.com', + proxy_client=self.proxy_client, + config=config + ) + + self.proxy_client.set_headers_addition({'X-Instance': 'value1'}) + with temporary_headers_addition({'X-Temp': 'value2'}): + service.run() + + call_kwargs = mock_post.call_args[1] + self.assertIn('headers', call_kwargs) + self.assertEqual(call_kwargs['headers']['X-Instance'], 'value1') + self.assertEqual(call_kwargs['headers']['X-Temp'], 'value2') + + +if __name__ == '__main__': + unittest.main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a879d95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "ai-sdk-python" +version = "0.0.0" +requires-python = ">=3.10" +license = "Apache-2.0" + +[dependency-groups] +dev = ["pip-licenses>=5.5.5", "commitizen>=4"] + +[[tool.uv.index]] +name = "artifactory" +url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" +default = true + +# currently not working +# [[tool.uv.index]] +# name = "artifactory-int" +# publish-url = "https://int.repositories.cloud.sap/artifactory/api/pypi/build-snapshots-pypi" +# url = "https://int.repositories.cloud.sap/artifactory/api/pypi/build-snapshots-pypi/simple" + +[tool.uv.sources] +sap-ai-sdk-base = { workspace = true } +sap-ai-sdk-core = { workspace = true } +sap-ai-sdk-gen = { workspace = true } + +[tool.uv.workspace] +members = ["packages/*"] + +[tool.pip-licenses] +# Blue Oak Council Bronze+ permissive licenses (https://blueoakcouncil.org/list) +# --partial-match is enabled, so each entry matches as a substring of the reported license string +allow-only = "Apache;MIT;BSD;ISC;Python Software Foundation;PSF;MPL-2.0;Mozilla Public License 2.0;CC0-1.0;Zlib;0BSD;CNRI-Python;Unlicense;Public Domain" +partial-match = true +ignore-packages = ["pylint", "astroid"] + +[tool.pytest] +markers = [ + "bedrock: mark a test as a bedrock test running in a different environment", +] diff --git a/scripts/teardown-integration-env.sh b/scripts/teardown-integration-env.sh new file mode 100755 index 0000000..8e0d717 --- /dev/null +++ b/scripts/teardown-integration-env.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Tears down the integration test environment. diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..11654d7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,5312 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] + +[manifest] +members = [ + "ai-sdk-python", + "sap-ai-sdk-base", + "sap-ai-sdk-core", + "sap-ai-sdk-gen", +] + +[[package]] +name = "aenum" +version = "3.1.17" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/e9/8b283567c1fef7c24d1f390b37daede8b61593d8cdaffb8e95d571699e83/aenum-3.1.17.tar.gz", hash = "sha256:a969a4516b194895de72c875ece355f17c0d272146f7fda346ef74f93cf4d5ba" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/8d/1fe30c6fd8999b9d462547c4a1bb6690bda24af38f2913c4bec7decb81f2/aenum-3.1.17-py3-none-any.whl", hash = "sha256:8b883a37a04e74cc838ac442bdd28c266eae5bbf13e1342c7ef123ed25230139" }, +] + +[[package]] +name = "ai-sdk-python" +version = "0.0.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "commitizen" }, + { name = "pip-licenses" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "commitizen", specifier = ">=4" }, + { name = "pip-licenses", specifier = ">=5.5.5" }, +] + +[[package]] +name = "aiobotocore" +version = "3.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "wrapt" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/75/42cce839c2ec263ff74b10b650fe36b066fbb124cbee6f247eac0983e1ab/aiobotocore-3.7.0.tar.gz", hash = "sha256:c64d871ed5491a6571948dd48eabd185b46c6c23b64e3afd0c059fc7593ada30" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl", hash = "sha256:680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a" }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb" }, +] + +[[package]] +name = "boto3" +version = "1.43.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/65/47670987f2f9e181397872c7ee6415b7b95156d711b7eab6c55f66e575bc/boto3-1.43.0.tar.gz", hash = "sha256:80d44a943ef90aba7958ab31d30c155c198acc8a9581b5846b3878b2c8951086" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl", hash = "sha256:8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b" }, +] + +[[package]] +name = "botocore" +version = "1.43.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417" }, +] + +[[package]] +name = "commitizen" +version = "4.16.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/cc/d87b094ef858c67febcd1d8902352c84b42c9ebc8221d6f2e9d553273358/commitizen-4.16.3.tar.gz", hash = "sha256:5cdca4c02715cc770312f4b505c65a6c39024c73ece41b943bccaf81c44436ed" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/35/c7995b1e66159193dd31ed5628d59acbaf4611811645eedf0fb2d5a91946/commitizen-4.16.3-py3-none-any.whl", hash = "sha256:ce1be39fe98a16725fd0c960daf0f360acac86db7ae8db1e1df8d3541005b5be" }, +] + +[[package]] +name = "coverage" +version = "7.14.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a" }, +] + +[[package]] +name = "dacite" +version = "1.9.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7" }, +] + +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.73.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/d8/40f5f107e5a2976bbac52d421f04d14fc221b55a8f05e66be44b2f739fe6/google_genai-1.73.1.tar.gz", hash = "sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/af/508e0528015240d710c6763f7c89ff44fab9a94a80b4377e265d692cbfd6/google_genai-1.73.1-py3-none-any.whl", hash = "sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c" }, +] + +[[package]] +name = "greenlet" +version = "3.5.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/21/117c8710abb7f146d804a124c07eb5964a60b90d02b72452885aecc18efa/greenlet-3.5.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7eacb17a9d41538a2bc4912eba5ef13823c83cb69e4d141d0813debe7163187f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/f7/6762a56fa5f6c2295c449c6524e10ce481e381c994cc44d9d03aef0700fb/greenlet-3.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5cc9606aa5f4e0bde0d3bd502b44f743864c3ffa5cfa1011b1e30f5aa02366f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/05/85a511e68ee109aff0aa00b4b497806091dd2d82ce209e49c6e801bd5d92/greenlet-3.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3d35f87c7253b715d13d679e0783d845910144f282cb939fe1ba4ac8616269c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/b8/8b83d18ae07c46c019617f35afd7b47aab7f9b4fbb12fc637d681e10bdd8/greenlet-3.5.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:540dae7b956209af4d70a3be35927b4055f617763771e5e84a5255bea934d2f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/14/ad1f9fc9b82384c010212464a3702bd911f95dab2f1180bc6fbcfb1f958c/greenlet-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed8cdb691169715a9a492844a83246f090182247d1a5031dc78a403f68ba1e97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/1c/43b8203cf10f4292c9e3d270e9e5f5ade79115a0a0ca5ea6f1be5f8915a7/greenlet-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d59e840387076a51016777a9328b3f2c427c6f9208a6e958bad251be50a648d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/6e/0344b1e99f58f71715456e46492101fd2daa408957b8186ade0a4b515da7/greenlet-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:b9152fca4a6466e114aaec745ae61cba739903a109754a9d4e1262f01e9259b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc" }, +] + +[[package]] +name = "idna" +version = "3.17" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.14.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661" }, +] + +[[package]] +name = "ipython" +version = "8.39.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f" }, +] + +[[package]] +name = "ipython" +version = "9.14.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/c2/c0064cf15d026501a1ef70e42efd9c3f818663089399aacc5e37a82901c1/ipython-9.14.0.tar.gz", hash = "sha256:6f27ff0f1d9ea050e0551f71568bc4b34d8aba579e8f111c5b4175f44ac6b4aa" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/a3/9e59340f02c1dc8f8c0a05b09244712b8609eb5439f9996e887e2b82f452/ipython-9.14.0-py3-none-any.whl", hash = "sha256:8fd984a3372c14b12790b084ba6b5cff5678c0cb063244a0034f06a51f20d6c2" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "importlib-metadata" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tabulate" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407" }, +] + +[[package]] +name = "langchain" +version = "1.2.18" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/4e/b651ecac63af474b28519384f7011294493d139937b1a9591581291eef34/langchain-1.2.18.tar.gz", hash = "sha256:7e829dbf117affadfd2067a0e97b4af20222f535f30fb812a28472d842c1074c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/20/959f6098c79158afe5aedce7de05c3700f10d293890ef9e5dace6c3ad94b/langchain-1.2.18-py3-none-any.whl", hash = "sha256:8432d43a65540845ed6f1a783d38d869c4659a6b9405f9a510169ad40d2f7bae" }, +] + +[[package]] +name = "langchain-aws" +version = "1.4.7" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "boto3" }, + { name = "langchain-core" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/a9/b705496c676daaf6f1ca1f3855a138d86d55570091823c2bd8b96ca4cc63/langchain_aws-1.4.7.tar.gz", hash = "sha256:2c0556b7549bdfaae2b68baa12fefcb2b869667540eb75dd0606edd629c4d543" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/52/cb/9bb552e8d9f736957a3cb798d519c3e9ae541295e0697b5b93c7fd5653dc/langchain_aws-1.4.7-py3-none-any.whl", hash = "sha256:1f6f5f70366781d61b35730d3cd5e927f7d5cbf7c44d3a023ee254598a96112a" }, +] + +[[package]] +name = "langchain-classic" +version = "1.0.7" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "langchain-core" }, + { name = "langchain-text-splitters" }, + { name = "langsmith" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/78/84b5065816f348c39fefa4316f209f0135e8410216340a953bec17d9e4e4/langchain_classic-1.0.7.tar.gz", hash = "sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/78/2d9980d028ff0523eea503a77c200e2ff252a3a75eb6e7842bcf5f9c979b/langchain_classic-1.0.7-py3-none-any.whl", hash = "sha256:d9d9be38f7aa534ed0259c2410432e34a1f80b1d491e686749bb55af56479be3" }, +] + +[[package]] +name = "langchain-community" +version = "0.4.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx-sse" }, + { name = "langchain-classic" }, + { name = "langchain-core" }, + { name = "langsmith" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "tenacity" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/0c/e3aca1f2b1c5b95f8b87cb2b6e81a6f20d538c07a128419dc01cef0617b6/langchain_community-0.4.2.tar.gz", hash = "sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/39/5d97e42a3e95dc2a6d71b2f902a3fae71786131e11d01bddb604accb0ebe/langchain_community-0.4.2-py3-none-any.whl", hash = "sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466" }, +] + +[[package]] +name = "langchain-core" +version = "1.4.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c" }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/52/de168715eb092c920531d418b8b9aafdff9e37ee80e5fc88106211ccbd47/langchain_google_genai-4.2.4.tar.gz", hash = "sha256:2f5de7a8a6552ffb64b907aca7503fd5e34d1a3240e280abcdc5f7eef480edd5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/a2/5feaf21cfe6fac80eae944f3ac5348d9e5e986813256f74f8dd104617474/langchain_google_genai-4.2.4-py3-none-any.whl", hash = "sha256:0e2c1021a15c91e60b68d813bb3e793bd1d9396b3f8639b943ab4e56e5652e04" }, +] + +[[package]] +name = "langchain-openai" +version = "1.2.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/1b/c506c7f41156d3a6b4582b4c487f480001b8741deecc6e2d4931fdf4cf2c/langchain_openai-1.2.2.tar.gz", hash = "sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/8e/7406c99afacafc8c2ce0fa4152f9f8b9598c93ceb291959821abd053b982/langchain_openai-1.2.2-py3-none-any.whl", hash = "sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.16" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "1.1.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10" }, +] + +[[package]] +name = "langcodes" +version = "3.5.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/75/f9edc5d72945019312f359e69ded9f82392a81d49c5051ed3209b100c0d2/langcodes-3.5.1.tar.gz", hash = "sha256:40bff315e01b01d11c2ae3928dd4f5cbd74dd38f9bd912c12b9a3606c143f731" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/c1/d10b371bcba7abce05e2b33910e39c33cfa496a53f13640b7b8e10bb4d2b/langcodes-3.5.1-py3-none-any.whl", hash = "sha256:b6a9c25c603804e2d169165091d0cdb23934610524a21d226e4f463e8e958a72" }, +] + +[[package]] +name = "langgraph" +version = "1.1.10" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/b3/7dec224369c7938eb3227ff69542a0d0f517862a0d27945b8c395f2a781f/langgraph-1.1.10.tar.gz", hash = "sha256:3115beb58203283c98d8752a90c034f3432177d2979a1fe205f76e5f1b744500" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/07/057dc1aa7991115fca53f1fa6573a7cc0dd296c05360c672cc67fdb6245b/langgraph-1.1.10-py3-none-any.whl", hash = "sha256:8a4f163f72f4401648d0c11b48ee906947d938ba8cf1f474540fe591534f0d17" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.13" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/a4/f8ac75fa7c503103f0cf7680944e28bbaaef74c19a8d163d7346869cc369/langgraph_prebuilt-1.0.13.tar.gz", hash = "sha256:ad219782a80e1718e7e7794de49e0ae307111d45cbcffab9a52725a66a609456" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/ef/5ada0bef4013ef5ae53a0ca1de5736517f1076a54d313f156ca545ec65d5/langgraph_prebuilt-1.0.13-py3-none-any.whl", hash = "sha256:7055e9fad41fbd3593800aed0aea0a6e974b17f33ed51b80d3d3a031212dd7c0" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.15" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/af/cdd4d6f3c05b3c1112ed3f12ef830faf15951b21d22cbc622a4becbbe25c/langgraph_sdk-0.3.15.tar.gz", hash = "sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/a5/0196d9c05749c25bc198e4909d68c998bc3120297e14944921baf2f4c384/langgraph_sdk-0.3.15-py3-none-any.whl", hash = "sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d" }, +] + +[[package]] +name = "langsmith" +version = "0.8.8" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/93/28df12b3b3c776077983b92f1299c623592b5999695af2a755fb90ff048b/langsmith-0.8.8.tar.gz", hash = "sha256:9d00e54f54d833c1914003527ff03ad0364741034330da72f0adbeaba852b6cf" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/71/94a8f2b573278a0b0b7dfd37663c0ddd36867f9e2bba69addd183de0cd56/langsmith-0.8.8-py3-none-any.whl", hash = "sha256:9d60d724c0d187c036e184b3ffdf9fa5c6822aa0bb88144a5fb898e79be645af" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56" }, +] + +[[package]] +name = "myst-nb" +version = "1.4.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "ipykernel" }, + { name = "ipython", version = "8.39.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.14.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-cache" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "pyyaml" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/b4/ff1abeea67e8cfe0a8c033389f6d1d8b0bfecfd611befb5cbdeab884fce6/myst_nb-1.4.0.tar.gz", hash = "sha256:c145598de62446a6fd009773dd071a40d3b76106ace780de1abdfc6961f614c2" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/93/0a378b48488879a1d925b42a804edfc6e0cd0ef854220f2dce738a46e7e9/myst_nb-1.4.0-py3-none-any.whl", hash = "sha256:0e2c86e7d3b82c3aa51383f82d6268f7714f3b772c23a796ab09538a8e68b4e4" }, +] + +[[package]] +name = "myst-parser" +version = "4.0.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d" }, +] + +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73" }, +] + +[[package]] +name = "openai" +version = "2.38.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2" }, +] + +[[package]] +name = "overloading" +version = "0.5.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/6c/dbc5ddff9eec6b57046c11985d28ebac2f8073596a7cc903c76f26856284/overloading-0.5.0.tar.gz", hash = "sha256:493f0f67211244ed6bf2acf9f3ac61fb38e8aa87834c4f0f84d8943512066588" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/d1/775dec1fb1e2a600d587adfef9aafba7b0b995532b3bc63a5a927446a8ce/overloading-0.5.0-py3-none-any.whl", hash = "sha256:c28d2a227cfb6bdefcfe0ded055bc620a5c784a52ecadc441f8d6c281b8bb1c1" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87" }, +] + +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1" }, +] + +[[package]] +name = "parameterized" +version = "0.9.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/49/00c0c0cc24ff4266025a53e41336b79adaa5a4ebfad214f433d623f9865e/parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/2f/804f58f0b856ab3bf21617cccf5b39206e6c4c94c2cd227bde125ea6105f/parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b" }, +] + +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e" }, +] + +[[package]] +name = "pip-licenses" +version = "5.5.5" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "prettytable" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/18/ddd93af610a04f56a51a27095ddfe55238e1ec236f6758730a0d2c0b49f2/pip_licenses-5.5.5.tar.gz", hash = "sha256:60750c006adf7a0910347b726e8ee9fee3bc8d2e7c8307a5c4ec0776c8e2a276" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/9a/6acfdb8d463eac7cdae7534d35d72237eca63f5fbafe797289d8a5fae447/pip_licenses-5.5.5-py3-none-any.whl", hash = "sha256:f4c4c6d9e6a03612cf59f29f19dc8ab54904d82e055b8e191498f2279a224e14" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, +] + +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" }, +] + +[[package]] +name = "pyhamcrest" +version = "2.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/3f/f286caba4e64391a8dc9200e6de6ce0d07471e3f718248c3276843b7793b/pyhamcrest-2.1.0.tar.gz", hash = "sha256:c6acbec0923d0cb7e72c22af1926f3e7c97b8e8d69fc7498eabacaf7c975bd9c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/71/1b25d3797a24add00f6f8c1bb0ac03a38616e2ec6606f598c1d50b0b0ffb/pyhamcrest-2.1.0-py3-none-any.whl", hash = "sha256:f6913d2f392e30e0375b3ecbd7aee79e5d1faa25d345c8f4ff597665dcac2587" }, +] + +[[package]] +name = "pyhumps" +version = "3.8.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/83/fa6f8fb7accb21f39e8f2b6a18f76f6d90626bdb0a5e5448e5cc9b8ab014/pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/11/a1938340ecb32d71e47ad4914843775011e6e9da59ba1229f181fef3119e/pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6" }, +] + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomlkit" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" }, +] + +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" }, +] + +[[package]] +name = "respx" +version = "0.23.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7" }, +] + +[[package]] +name = "roman-numerals-py" +version = "4.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e" }, +] + +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049" }, +] + +[[package]] +name = "s3transfer" +version = "0.17.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/b3/bcdc2f58fa92592db511beda154c2c08d28f21f6c4637f06a42a24b10c21/s3transfer-0.17.1.tar.gz", hash = "sha256:042dd5e3b1b512355e35a23f0223e426b7042e80b97830ea2680ddce327fc45e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl", hash = "sha256:5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c" }, +] + +[[package]] +name = "sap-ai-sdk-base" +version = "3.4.1" +source = { editable = "packages/base" } +dependencies = [ + { name = "aenum" }, + { name = "pyhumps" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "aenum", specifier = "~=3.1" }, + { name = "pyhumps", specifier = "~=3.0" }, + { name = "requests", specifier = "~=2.32" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pylint", specifier = "==4.0.5" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-cov", specifier = "==7.1.0" }, + { name = "pytest-dotenv", specifier = ">=0.5.2" }, +] + +[[package]] +name = "sap-ai-sdk-core" +version = "3.3.1" +source = { editable = "packages/core" } +dependencies = [ + { name = "click" }, + { name = "sap-ai-sdk-base" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyhamcrest" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = "~=8.3" }, + { name = "sap-ai-sdk-base", editable = "packages/base" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyhamcrest", specifier = "==2.1.0" }, + { name = "pylint", specifier = "==4.0.5" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-cov", specifier = "==7.1.0" }, + { name = "pytest-dotenv", specifier = ">=0.5.2" }, +] + +[[package]] +name = "sap-ai-sdk-gen" +version = "7.2.0" +source = { editable = "packages/gen" } +dependencies = [ + { name = "click" }, + { name = "dacite" }, + { name = "h11" }, + { name = "httpx" }, + { name = "langchain" }, + { name = "langchain-classic" }, + { name = "langchain-community" }, + { name = "langchain-openai" }, + { name = "langcodes" }, + { name = "openai" }, + { name = "overloading" }, + { name = "packaging" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, + { name = "sap-ai-sdk-core" }, +] + +[package.optional-dependencies] +all = [ + { name = "aiobotocore" }, + { name = "boto3" }, + { name = "google-genai" }, + { name = "langchain-aws" }, + { name = "langchain-google-genai" }, +] +amazon = [ + { name = "aiobotocore" }, + { name = "boto3" }, + { name = "langchain-aws" }, +] +google = [ + { name = "google-genai" }, + { name = "langchain-google-genai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "myst-nb" }, + { name = "parameterized" }, + { name = "pillow" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-dotenv" }, + { name = "requests-mock" }, + { name = "respx" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxawesome-theme" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiobotocore", marker = "extra == 'amazon'", specifier = ">=3.2.0" }, + { name = "boto3", marker = "extra == 'amazon'", specifier = ">=1.40.61" }, + { name = "click", specifier = ">=8.1.7" }, + { name = "dacite", specifier = ">=1.8.1" }, + { name = "google-genai", marker = "extra == 'google'", specifier = "~=1.73.1" }, + { name = "h11", specifier = ">=0.16.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "langchain", specifier = "~=1.2.14" }, + { name = "langchain-aws", marker = "extra == 'amazon'", specifier = "~=1.4.0" }, + { name = "langchain-classic", specifier = "~=1.0.0" }, + { name = "langchain-community", specifier = "~=0.4.1" }, + { name = "langchain-google-genai", marker = "extra == 'google'", specifier = "~=4.2.0" }, + { name = "langchain-openai", specifier = "~=1.2.0" }, + { name = "langcodes", specifier = "~=3.5.1" }, + { name = "openai", specifier = ">=1.66.0" }, + { name = "overloading", specifier = "==0.5.0" }, + { name = "packaging", specifier = ">=23.2" }, + { name = "pandas", specifier = ">=2.2.0" }, + { name = "pydantic", specifier = "~=2.12" }, + { name = "sap-ai-sdk-core", editable = "packages/core" }, + { name = "sap-ai-sdk-gen", extras = ["google", "amazon"], marker = "extra == 'all'", editable = "packages/gen" }, +] +provides-extras = ["google", "amazon", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "myst-nb" }, + { name = "parameterized", specifier = "==0.9.0" }, + { name = "pillow", specifier = "==12.2.0" }, + { name = "pylint", specifier = "==4.0.5" }, + { name = "pytest", specifier = "==9.0.3" }, + { name = "pytest-asyncio", specifier = "==1.3.0" }, + { name = "pytest-cov", specifier = "==7.1.0" }, + { name = "pytest-dotenv", specifier = ">=0.5.2" }, + { name = "requests-mock", specifier = "==1.12.1" }, + { name = "respx", specifier = "==0.23.1" }, + { name = "sphinx", specifier = "<9.0.0" }, + { name = "sphinxawesome-theme" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/ee/67eef9600338e245ad7838230969a34c823ddbdbccc5e1fc43cd75b55bc9/snowballstemmer-3.1.0.tar.gz", hash = "sha256:fd9e34526b23340cd23ffea6c9f9760974ecc2c2ac9e1d81401443ccdb2a801f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/83/ddbf4533c62dd32667ef1238952abef155f3d3391f5be69a352ad1638a42/snowballstemmer-3.1.0-py3-none-any.whl", hash = "sha256:17e6d1da216aa07db6dad37139ea70cf13c4b2e9a096f6e64a9648fc657d3154" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3" }, +] + +[[package]] +name = "sphinxawesome-theme" +version = "6.0.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/15/96d7b46d987d9894d5fc69328f05596f6c9ca3a8cafc28ea73a250ba4c1c/sphinxawesome_theme-6.0.2.tar.gz", hash = "sha256:984c1a107584eb6a913ba4c2e4c74b23212fbffeee9ef60688810b46823ade25" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/d4/6bcc860549d4894fc30c79be0224f60ee190e37bdcb59a715c247358bbe6/sphinxawesome_theme-6.0.2-py3-none-any.whl", hash = "sha256:a35dcabe3b0906aff270e1d18948f93040769e76551a2eb26cf26b29e06d1e51" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.50" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/a9/812a775bd8c1af0966d660238d005baf25e9bced1f038c8e71f00aa637a7/sqlalchemy-2.0.50-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af6eeb84985bf840ba779018ff9424d61ff69b52e66b8789d3c8da7bf5341b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/74/5a6bc5496e9be8f740fbf80f9e6bd4ab965c8a80870eb07ab015e360957a/sqlalchemy-2.0.50-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fe7822866f3a9fc5f3db21a290ce8961a53050115f05edf9402b6a5feb92a9f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/55/b260d8df2adc9bb0bf294f67b5f802ff0d84d99442b536b9efd0ea72d447/sqlalchemy-2.0.50-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e1b0f6a4dcd9b4839e2320afb5df37a6981cbc20ff9c423ae11c5537bdbd21" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/6d/58714005cbf370f16c3f30d30324a43be10069efcfe764f7236a2e851947/sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e195687f1af431c9515416288373b323b6eb599f774409814e89e9d603a56e39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/e8/67527fee039bd3e1a6ce3f03d2b62fd87ab9099c17052810d79496727b66/sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea1a8a2db4b2217d456c8d7a873bfc605f06fe3584d315264ea18c2a17585d0b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/b2/dd3155a6a6706cb89adecf5ee6e0512f7b0ee5cf3e6f4cde67d3c20ebfda/sqlalchemy-2.0.50-cp310-cp310-win32.whl", hash = "sha256:68b154b08088b4ec32bb4d2958bfbb50e57549f91a4cd3e7f928e3553ed69031" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/a1/a09c463ee3e7764b5ce5bd19a7f0b6eefbde62e637439ab58498cdbd6b47/sqlalchemy-2.0.50-cp310-cp310-win_amd64.whl", hash = "sha256:66e374271ecb7101273f57af1a62446a953d327eec4f8089147de57c591bbacc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/5d/3172686af1770e4de2805f919a51441085f589ddadf3dd76ec582f84f497/sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/90/e98dedea3c3e663a17afcd003a34ba45efdac2cea3b6f2e4585e2b1e2537/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/4f/501308c2babb62c11753ecb4ee88ba9eef019419a4d6cbf7cb13e2bad353/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/39/d88996c5e03ed6248c3a788d20f0b8d8b376b9f8a495e4bab9df7c72d2f8/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/1b/1ae0e65161b51cc43e5ca75430ef79d80e23b5042d645586c2c342c3b92e/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/29/17c0003f2c0dfa6d1b97672475707e3ec5980db09defd7fa20beb6833bbd/sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/18/280d00654cc19d1fccf236fa5070f6dd04b84dde6f1b2e637bde0ff340a7/sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738" }, +] + +[[package]] +name = "tornado" +version = "6.5.6" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/57/6d7303a77ae439d9189108f76c0c4fd89ee5e2cc8387bffb55232565c4ed/tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/0d/b4f481e18c5a51864e6d12b9a05ecf72919696680b747c958c3fc1f4fbae/tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/9c/5430c39fcab1144d35860f457b15e9c08b4bc7ac86764354204e983d6183/tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/79/fa7e14a2f939c807a8d30619b4eb604eab219601b78792516ebe22d40cf9/tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/71/bd67d5f5199f937dafe03a49a37989f60f600ff6fef34c79412a829d97bd/tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/a4/c24388c9cf5b3c3a513b56a158af9f23092c9a2810d789e294310797df21/tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/eb/6a07ad550c3f7b37244bd0becdf293ec3d3e961783d8b720a97df50de1b2/tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/84/3469e098dccdb6763130e06aacd786bb4363fca7b590a55c101ddf34ed30/tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/3c/273a04e0b9dd9016f1685cca0c1c8795a71ac88a34a8c889a0b443483226/tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/98/0cffe22a224f60c5fb1e3aa0b76f9da2e1ca78b0e9545e3d077c68ce60a7/tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf" }, +] + +[[package]] +name = "traitlets" +version = "5.15.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, +] + +[[package]] +name = "uuid-utils" +version = "0.16.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/78/fc830a25597001586770f0436a4917aac21fcdaf7ac2824bbe168ccdc724/uuid_utils-0.16.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a632fead2a6505a8df3318d5e95503739b9aa1c518521cd93d83ce00699b78f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/39/3f1eee6d3c3c33d6dd75441bdb49ac246de57f97f67faa7ff04cdb5e4ffe/uuid_utils-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d716e5b35266400d2a2cd349697868179825f113c543e55c9d2ac304991f8d4f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/85/f7fb16eed216fd8085d62d4ce7179e2a81ac7649e043f34168e7700b6df4/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:207c2a98ca8b065cc93378a3a59744efb88a68e9ecc2c3afefe43d59c864280a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/ea/b2b629d29c8234677850e1ae47add9c8866dfb3864af257542989a13ba1b/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79824850330e450c7b2fa933572e32192240060937426052fa3fc05134ed3faa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/8e/a6871c6231244bb80be06a2babf3ca34396b29d893103d84ddfd3654e6e4/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d89927c47e1a55509e90b7f2fd3e7ff89908c77b61f8f0deda97a89d8854e0f8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/d0/b606a2857f98c20c149044e80f276ff7966c9f679fc7b25f6d608bd8d48b/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ae4168e1ca0ae69d24207645a8b3cd2b641a0ad15058eda17d2c9898aa89d3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/e1/7951dd47b6717b6ebb340e673d31d539be928d280a697fab4dd233bcc7fa/uuid_utils-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d363017a3223de3a57eb6fca135df6ffcef7c534836bff2e71354dce7d10987c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/5d/f46e91fad5f049c7bd12701293c1ac31b4460ec83606c4bdd37c05abef52/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4a87a7433b355eadaa200f150da6bb5b87bb6de0adf260883b26cb637aba0410" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/94/ea4f559e5e87da5847ecf78ba68a78e8bb4e537e1169093ea543cab94886/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6da070e75b0e2424728e6f8547647cce36c83f9a6101a08da4849a8ab2b58105" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/41/60dbac2459426a925b77e08cb8ec492d4bc82caa0f124f498d2e24409cb8/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1baab8966f9e0097cbaf9cc01ad448b38e616e7b4968ca5e49cb53a74ad91a2f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e8/90/ae39c1e1bff65dfe9c7c70cbd64b8d529a3d1cc836aeaa7accdc44e5c308/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b42014536943c1a654ff107538c0f7dc39809d8d774ec8dafd19bec05006e568" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/5c/4dc93017a095c9c314525a9abc4f9983e520d88d7eff9bd52398d81c374e/uuid_utils-0.16.0-cp310-cp310-win32.whl", hash = "sha256:228701ab6f188b6def24f2add6db64f0794adb1f06d0abacdcec40b0cda13cdf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/df/1398f5b117d5daa4d757b156728db7aa092a3eff1271c40ec39dbe945327/uuid_utils-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10d3c5983f770b1b2847ad811c87a1c9e28f8155d1a27cc581abcd5abb386b64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/24/0e18177e2fbb0b9f54f90fd48fe3302dfda731e22ad650d6e6f8f4b3d3d3/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/7e/bb91b04b2c8a081a4df2d50f1a50dd85502e2391c6eaed71b339ec9f2524/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3d86ca394e0ea21bdb53784eb99276d263b93d1586f56678cab1414b7ae1d0f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/69/2a/47ee18b294af59754ef5acfa96eb027137c98cef7521199b6f70be705de4/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/7c/ed6d8bb48eeecaed6722af1187d722c5243334be750419d10d5f05dffeb2/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/33/371bddf9fd47e045c375df9668eea0d96ce9201ab6a03985b0155498e376/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dc/f1/b201d5ee005d4987fc072714fcb9f6e75303520cf19d4deec0b4df44bf40/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/6a/04b4c02ce5c24a3602baa12e59bd3ec853ae73c3e9319b706c4620f47a05/uuid_utils-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:948485c47d8569a8bf6e86f522a2599fa9134674bee9f483898e601e68c3caca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/19/25db019727d14630c75c2a75a8ea66dd712bb468adcf410bac8d01ff19fd/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/93/c000cd42ebfdd37cc74981ed31c979a1270156572bdebab8b5d61460e750/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/1d/7dd239909c82616722b9ee53fa1b4657c6244fb4fd026890300ebf6db22b/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1c2df42314b014c9d23330f92887e21d2fc72fde0beb170c7833cd2d22d845a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/49/b6a688648368a9cc0137e183657956853a91dc06ef73deda27290d586155/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/fb/34f221ae93d5ea249a0d7056bdf45313b8d267d6aa9c5d0673ac1a4746c7/uuid_utils-0.16.0-cp311-cp311-win32.whl", hash = "sha256:733da81d51ea578862d8b9b754e8968b6da2be2b7840aee868917c23cae84015" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/70/c2a608a813f655834ee6df4ce53ea46edad4d54f774eac1890be5c7e4e1c/uuid_utils-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:10d21fddb086e69245c4f0f77c7b442471f3a242aa85f62954bff157baa1c5f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/c3/8ab4eff328a833c065f280b2e0d9ac873505b5e5282f2bc5133a9843d4dd/uuid_utils-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:98e2404713677070cee9a99a1f1e24afd496c18e833ee1b31a0587659452ff80" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/4c/b4cf43a5d22bcdb91727acdf54be0d78e83e595b73c5a9a8a4291875f059/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/fb/4b0d1c4b5e9f8679ca41b9cdbce5749e1d5db3d3d42a07060d6ce61ac583/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66a9c8cedf7695c28e700f6a66bde0809c3b2e0d8a70968be7bfd47c908952e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/43/2dc6c7401c8fab86e46b0b33ada6dcfde949b2fd48877ba6f880862be80e/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/f5/48f11fb91f36453611ca148bc441436f279870b1ec6b576dc5167fb6e680/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/cb/b2b49528521e4a097f129e8bf7850a26f00af46afba778832cf3458a5c00/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/b3/a28d9c6f7c701dfe01c8020b30e33899a28eb9e4d056b07e7388f50ebf67/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cf/65/e1ff41dc44966e396ead86e104ba21b35ddb07ff7a64bb55013074ee77fe/uuid_utils-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2e981b1258db444df4cf4bf4c79673570d081d48d35f22d0f86471e0ad795c5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/57/fb19b7951f66a46e03bd1943a61ee9d59c83e994e56e8c97d79aff1f0e47/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2f/8e/9a129c469b7b77afb62da5c6b7e92591073b845bd0c3108c0d0aa65389fb/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/56/2ef71fad168cc3d894f7094fa458086c093635d7835381c91470b19c9ad3/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:38126b353527c5f001e4b24db9e62351eb768d0367febcd68100a4b39a035109" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/bf/68e60ea053ca30f35df877b96001331398140d5c4983561affa1350331b1/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/19/b521f7d73094fca4c0c44002f4a42bfcbcf0b770fdc3c4b9a596dda25734/uuid_utils-0.16.0-cp312-cp312-win32.whl", hash = "sha256:52d2cc8c12a3466cd1727883e0746d8bad5dddd670369eb553ba17fdc3b565ca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/1f/4126c3ccbc2d98a613664e55f6ab6d7bd4b98424a04486e4fcc76549af15/uuid_utils-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97625e5edfda8b118160ce1e88756f92b1635775f836c168be7bf10928d97fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/62/b83ccc8446ae39dcc0bda2cb3b525b6af6a2036383afe1d1d5fe7b234c2c/uuid_utils-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:baf79c8050eb784b252dd34807df73f61130fe8676b61231baccab62530f20ec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/1c/009e37b70f1f0ff17e7103a36bafde33d503d9ea7fe739761aa3e3c9fde6/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/11/4a/bf841ba90f829c7779d82155e0f4b88ef6726ccc25507d064d50ac2cd329/uuid_utils-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/4e/7c8cf03ec15cd6f40e4cbab81b2b4a625461327f68c7971e54723280ec3e/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/a7/cd1adbea7ef882a70db064c00cd93b12e11027b4cdd7ffd79e95c35fc3e3/uuid_utils-0.16.0-cp313-cp313-win32.whl", hash = "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/99/617ceb9e3a95b23837012740979baf71afad723b70daf34862da3f7c17a1/uuid_utils-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/d8/148ae707bfc36d482e39db679c86b81bdce264d4feb9df5d40a03b7687e3/uuid_utils-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/8c/b9a0462c38535c1662acb1025768e2d626bee5ce9e1790bad6b5381162ea/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/ef/2c719b2c26bb5b5e5061a1435c11ad2bd33ac3cd6d4cd0c7c3ac1d3396ed/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f1/1e/9a9fa261edf4c972f28ae83421377e3ab8dbd0bd7db58fd316e782d09a3b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/0c/18945f417d6bb4d0dd2b7652fe36c58c4e83bcf593b9b326b83aa40b853a/uuid_utils-0.16.0-cp313-cp313t-win32.whl", hash = "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/cc/c0eb0c3fab2ed80d706369b750029143b53126809b77b36bcbb77da66bab/uuid_utils-0.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/77/50ac87b6e18b1c686f700aa38c9471a990683c6a955f71ac1a6677ed8145/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/83/16/65046676de246bb5334d9f58aa96d2feb9fc347fda3556aaff7da1c2fc7a/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/d6/54fa988606a15dfd2028e925d8eb9c3ee6edbf1eb7692a67b37282880b56/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/1b/50622f967ceacea1f89fd065d9bfd395b51acb02cfb0a4ddc8fa9ff0c983/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/f5/4059706be6617e2787e375ea52994ce3c3fa3920b7d4a9c8ebf7895681a5/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/d5/f44b2710563da687a368f0ce4dcbd462dfb6708bcd46439d831991d595c7/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/a7/a69e859e37d26c5603f0bc0ae481860f691224f140e5a832f325b804770d/uuid_utils-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/73/4139cd3ca7b81ea283c1c8769373e9b2008241c0744a8ffb25f0a1b31325/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/8c/858101583fbad1b3fa04da88b1f7170836aa0f00b4cb712063325c44466d/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5e/bd/8f3d54a4763dd91ebd0f3d7b0c2ec434e4e0b1fc667b03a44d611a465ec6/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/76/4c9a8d9baaa243c7902d84dbba4d51b1ab51c379c66d3fd6368ff6933ecf/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/13/d32cea997f880cedde415730ce0e872ebfd7a040155ae0bbda70eccd208e/uuid_utils-0.16.0-cp314-cp314-win32.whl", hash = "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/19/9fc55172d8fe59e1f27a14d598b427fa508a7ebb35fa7b7b99c24fa0ef13/uuid_utils-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/5d/fcd9226b715c5aa0638fcdd6deaf0de6c6c3c451c692cd76bfca810c6512/uuid_utils-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/64/97ec9af95e58b8187f2934008ffab26e1604d149e34fe01c388b0543a24f/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/6d/e4082f407484ac28923c0bf8e861e71d277118d8b7542d0a350340e45350/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/43/c5c5f273c0ff889f20f10344784f9197dd00eb81ccc294330d4b949fea7e/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/7f/669aa899ab5378374d28a28231e6978f739921a1af394c7ebd6cc86e2639/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/57/a2a32406d79a222794ef98a19254fd9a81a029a0f32d7740fba9873bff1f/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/6b/85459a35bfa7d73e79acbc4eab1cf6aa6e4d9d022c3260ed9dea539c7f0b/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/9e/e965efdbb503ed14d6e57aec1a22b98326ed24cc2fb48e750c4d192267a0/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/ae/4321867888a783d03b7c053c0b68ca45d03974d86fcebf44d4ec268db397/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/9a/914a47bf42479bff0ce3e1fa1cbe3585354708edc928e27687cf91de9c26/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/4c/2abacd6badba61a047eaa39c8347656229d12843bd9bbe4906daa6dc752c/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/1f/9d1a09521276424da19dc0d74456aed3311170fec181b28fa6acba45d963/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/22/14dbedb6b61f492d5524077fd10bbfb137583b0f0aafa6cd870ccb43f39a/uuid_utils-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/25/f4/a636806c98401a1108f2456e9cc3fa39a618145bfb1d0860c57203159cfe/uuid_utils-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/89/655408a5485c56bf2c4561eb85f5bca119b1f4020370b4daaeb8d13e46fb/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/1c/a7c5506a4e2cf95ac98fec0996c56daa14e41f2ab1858f569b3556a202f9/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b35706350cf9bd4813f1811bebe03cac09795a5a379f90cb3616171f4e9ffc9e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/75/4267ab8baa1e6a8ad7c262e204484b44df0fde0920025ea9b43c2b869726/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/77/c794102831e331564f651099cac55006694677938d70f1033b35da451a89/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/3e/458a0a2da75c596b151182a6c7550c6c3d30f479e14e40f69c0336579e59/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/15/dd1fab6f7fcd15f2c331d0c1f0f516bb1113a640216460f82be53db3dcf8/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/56/62dcd551b140cbeb0f87522da2015b4b9e5818327b920506ad88d28562b0/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abfbf5e0c47fb31b37164a99515104e449a0bee36a071dc8b105457a2b35a5e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/e7/3937b9a9d6745b94dbe7b86531e098db8c53b77c8d07df7daa9577a47b8e/uuid_utils-0.16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:680799a9ade01d69c53cb9d41392ced24919d4f600bfab5060b61fca37510097" }, +] + +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec" }, +] + +[[package]] +name = "wrapt" +version = "2.2.1" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/8b/84bc1ea68b620fe0e2696a8cff07e82f4b962d952ab14efee8955997bb70/wrapt-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0f68f478004475d97906686e702ddbddeaf717c0b68ad2794384308f2dc713ae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/8f/64ec81194a0bc708d9720174c998c8a32116e82b5b32c04e20a7fe01176c/wrapt-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e422b2d647a65d6b080cad5accd09055d3809bdff00c76fba8dca00ca935572a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/94/c2/3d186944aae923631d1def58f4c4ff8f0b6309906afc0b6978de3e69b3e0/wrapt-2.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:036dfb40128819a751c6f451c6b9c10172c49e4c401aebcdb8ecf2aec1683598" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/01/d1/6b3d0ea995b867d2862aad5619bd5e17de09a9d64a821f46832dcd272d40/wrapt-2.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09ac16c081bebfd15d8e4dfa5bdc805990bbd52249ecff22530da7a129d6120b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f9/4b/37ecb90a8c3753e580327fb40731a984b754e3df65d2ef932bf359fe4adc/wrapt-2.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07be671fa8875971222b0ba9059ed8b4dc738631122feba17c93aa36b4213e9a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/d0/918884d9dfa84d0d135b42a51c00910f5c5447fe7a5e211a8e16ac324dd4/wrapt-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93fc2bf40cd7f4a0256010dce073d44eeb4a351b9bca94d0477ce2b6e62532b3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/00/382299d8ced610b29b59b099a89eda821e8c489aa152b7183748ac83f32a/wrapt-2.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba519b2d765df9871a25879e6f7fa78948ea59a2a31f9c1a257e34b651994afc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/46/62a79b79e35bbebb1207ca5d15b81192f37f20cc5659cf4e3ce955b7fcc8/wrapt-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9011395be8db1827d106c6449b4bb6dd17e331ff6ec521f227e4588f1c78e46f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/db/95c152151d206d4b430516c89725306e92484072f38e65492afde63f6d19/wrapt-2.2.1-cp310-cp310-win32.whl", hash = "sha256:a8f7176b83664af44567e9cc06e0d3827823fcc1a5e52307ebb8ac3aa95860b9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/d3/882d50452c6fbd13f24fe5d2644b97cdad2565a7e1522cbb6312de8a52cf/wrapt-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:d7f513d3185e6fec82d0c3518f2e6365d8b4e49f5f45f29640d5162d56a23b54" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/0f/148376523b4e370692286a9ba14d5715cf3c5b86da3bd3630926367b6b73/wrapt-2.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:44255c84bc57554fed822e83e70036b51afa9edb56fc7ca56c54410ece7898c9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/ac/4370bde262c0e633e6c4f0e56d55095710024cf9a5cecc20c59a10de483c/wrapt-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd57607acc85678925940bd5df0385ff8332083a32fa8d7a43f8767f4997263c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/eb/79/b8ff3a61e71babf58a8cf4c0d63358e8bad383e15bf7f35e62d2f6b6e4a4/wrapt-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ae574d65c9fa8e86f64f6a7c2668f9fcd507b183e0e577619f504b883cb0a6c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/fd/c0cac1f77c9c4f6fe58a920ca632ce379bb8be928720e11e8d73de28a5e9/wrapt-2.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a04c28c10ba7fd12842b109d2edb0678872a2fe65277ca4ff06a0d61edee245" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/4f/744132a7b2fbefa6b81118ec5942eca5fc2e9a129f9055a0c5e46885a549/wrapt-2.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2f02472a1cbbf3884b365714a810b5947134a95ad6952b554cb8cce9d492b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/95/b7cd9a22a06cf93e6482904ee6afc956248983553593fd1009296d1b3b31/wrapt-2.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac2745950b2bff80219c15ebf2fa9d8427eba7e249739f97e55c9d169e47e9e1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/4a/eb79423192015f46f0db2872e7e04a3dde8d359b83411e8959e7c9287eaa/wrapt-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a97e5b6c457f0cd3cfc19ebb2d84463e60c3ece754cc831e4281a3ca29bb18" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/dc/435015b58ce33c6fc4104158fa91ddb0e809ab03a5751fb7465d1d461456/wrapt-2.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c803a3d331796255af51ba2c79ed0ac8275865b516c09e61f248d1e7aff31ce9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/ac/5d203f98df8fd136b95c5227139aea02d34505e18baf812d0c005df61963/wrapt-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b984d1eb252145d6302c1dbd5e87fc6d404d45531447c84eadec04bf1fcb027" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/52/2f/a92427dbdc74e54c1674abbed27e61b2cb5e7a94441b8c1270c70671d928/wrapt-2.2.1-cp311-cp311-win32.whl", hash = "sha256:8a983a603a18c8708f024f7f6991b2e66159219abbf894634c5056243c55f3cd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/56/987b9c13b3e1c1a3c6de71284076f996b79caec90e75a87c044a40c23db9/wrapt-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c210a6994b21aa9b29e81c8d11560e8fdab54c117e9cff37870d0a27bde1343" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7e/25/d01f560888d99d94a959c85533de349ce68d71ace3f2591d6ea8f632cfed/wrapt-2.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:401229e9d63ca09f9b8891ecf83798d26c11bbb445d11ed9f1836b6d4585b38a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f" }, +] + +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/49/e4b575b4ed170a7f640c8bd69cfadfa81c7b700191fde5e72228762b9f73/xxhash-3.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd8ab85c916a58d5c8656ea15e3ce9df836fe2f120a74c296e01d69fab2614b4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/61/40f0155b0b09988eb6cdbfc52652f2f371810b0c58163208cb05667757bd/xxhash-3.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85f5c0e26d945b5bb475e0a3d95193117498130baa7619357bdc7869c2391b5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/bd/2902b7aad574e43cd85fd84849cfbce48c52cb02c7d6902b8a2b3f6e668e/xxhash-3.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7ffeaada9f8699be63d639536b0b60dff73b7d3325b7475c5bc8fdbf4eed47f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/df/343ce8fd09e47ba8fba43b3bad3283ddf0deca799d5a27b084c3aa2ce502/xxhash-3.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee88dfaa6b1b2bfadd3c031fa5f05584870e62fb05dc500942e9900c44fcfda" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/cf/703e8422a8b52407864281fb4eb52c605e9f33180413b4458f05de110eba/xxhash-3.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7426ff0dfa76eb47efc2cc59d4a717bfa9dc9938bff5e49e748bca749f6aa616" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/bc/d4b039edbd426575add5f217abeeb2bf870e2c510d35445df81b4f457901/xxhash-3.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8ff6ec73110f610425caef3ea875afbfc34caa542f01df3a80f45aadeb9f906" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/42/24/c6f81361796814b92399a88bf079d3b65e617f531819128fcf1bd6ef0571/xxhash-3.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d23fd49fdc5c8af61fb7104f1ad247954499140f6cb6045b3aa5c99dadbbf28" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/db/268012153eb7f6bf2c8a0491fdcde11e093f166990821a2ab754fe95537d/xxhash-3.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c249621af6d50a05d9f10af894b404157b15819878e18f75fcbb0213a77d07" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/86/1d0d905d659850dad7f59c807c130249fdb204dc6f71f1fb36268f3f3e61/xxhash-3.7.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6741564a923f082f3c2941c8bb920462ed5b25eaebdd1e161f162233c9a10bc5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/52/fc01ca7ff425a9bdb38d9e3a17f2630447ce3b45d45a929a6cd94d469334/xxhash-3.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4fd8acc6e32596350619896feb372033c0920975992d29837c32853bb1feacd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/96/122e0c6a3537a54b30752031dca557182576bae1a4171c0be8c532c84496/xxhash-3.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:646a69b56d8145d85f7fd2289d14fba07880c8a5bda406aa256b407481a61f35" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/17/92e33338db8c18add33a46b56c2b7d5dcc6cc2ac076c45389f6017b1bf37/xxhash-3.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:11dd69b1a34b7b9af29012f390825b0cdb0617c0966560e227ca74daa7478ba9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c7/04/fd4114a0820913f336bef5c82ef851bde8d06270982ebd7b2a859961bbf2/xxhash-3.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:01cf5c5333aed26cc8d5eea33b8d6398e085e365a704b7372fabdf7ab06441a9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/dd/eb/a2472b8b81cd576a9af3a4889ad8ba5784e8c5a04592587056cdaededd6c/xxhash-3.7.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f1e65d52c2d526734abecb98372c256b7eacce8fdc42e0df8570417fb39e2772" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/d3/493afc544aae50b5fb2844ceaeb3697283bb59695db1a7cb40448636de05/xxhash-3.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8ff00fcc3eb436617ed8556cf15daf76c2b501248361a065625a588af78a0a02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/6a/002800845a22bff32bcf5fd09caceb4d3f5c3da6b754c46edb9743ce908b/xxhash-3.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b5cd29840505631c6f7dbb8a5d34b742b5e6bbda38fe0b9f54e825f3ea6b61dc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/0f/86ee514622a381c0dc49167c8d431a22aa93518a4063559c3e36e4b82bc8/xxhash-3.7.0-cp310-cp310-win32.whl", hash = "sha256:5bf2f1940499839b39fef1561b5ecb6ede9ac34ef4457474e1337fc7ef07c2f3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/86/45/2ef2310803efb4a2d07844e8098d797e25702024793aa2e85858623a43b5/xxhash-3.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:d41fcda2fa8ca682ebca134a2f2dc02575ba549267585597e73061565795f475" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/75/40dbf8f142baf8993c38cd988c8d8f51fe0c51e6c84c5769a3c0280a651d/xxhash-3.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:a845a59664d5c531525a467470220f8edc37959e0a6f8e734ffb6654da5c4bee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/f4/7bd35089ff1f8e2c96baa2dce05775a122aacd2e3830a73165e27a4d0848/xxhash-3.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdc7d06929ae28dda98297a18eef7b0fd38991a3b405d8d7b55c9ef24c296958" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/2f/eeb942c17a5a761a8f01cb9180a0b76bfb62a2c39e6f46b1f9001899027a/xxhash-3.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9e6c0d843f1daf85ea23aeb053579135552bde575b7b98af20bfc667b6e4548d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/08/e1/67f5d9c9369be42eaf99ba02c01bf14c5ecd67087b02567960bfcee43b63/xxhash-3.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f420ad3d41e38194353a498bbc9561fd5a9973a27b536ce46d8583479cf44335" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/7c/8cb34b3bed4f44ca6827a534d50833f9bc6c006e83b0eb410ac9fa0793bd/xxhash-3.7.0-cp311-cp311-win32.whl", hash = "sha256:3281ba1d1e60ee7a382a7b958513ba03c2c0d5fcbd9a6f7517c0a81251a23422" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/47/a49767bd7b40782bedae9ff0721bfe1d7e4dd9dc1585dea684e57ba67c20/xxhash-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7f25baec4c5d851d40718d6fae52285b31683093d4ff5207e63ab306ccf14a5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/c6/3957bfacfb706bd687be246dfa8dd60f8df97c44186d229f7fd6e26c4b7e/xxhash-3.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:4c2454448ce847c72635827bb75c15c5a3434b03ee1afd28cb6dc6fb2597d830" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/54/c1/e57ac7317b1f58a92bab692da6d497e2a7ce44735b224e296347a7ecc754/xxhash-3.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad3aa71e12ee634f22b39a0ff439357583706e50765f17f05550f92dbf128a23" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/ca/a9c78cb384d4b033b0c58196bd5c8509873cabe76389e195127b0302a741/xxhash-3.7.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7fbec49f5341bbdea0c471f7d1e2fb41ae8925af9b6f28025c28defd8eb94274" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/5f/4acfcd490db9780cf36c58534d828003c564cde5350220a1c783c4d10776/xxhash-3.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec101643395d7f21405b640f728f6f627e6986557027d740f2f9b220955edafe" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/simple/" } +sdist = { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b" } +wheels = [ + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2" }, + { url = "https://common.repositories.cloud.sap/artifactory/api/pypi/pypi-proxy/packages/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d" }, +] diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..7cdb861 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,8 @@ +rules: + dependabot-cooldown: + config: + days: 4 + unpinned-uses: + config: + policies: + 'SAP/ai-sdk-python/*': any